Compare commits

...
85 Commits
Author SHA1 Message Date
hacker07 31dd4d575b подпись релиза 1.2.1.9
Синхронизация с автором / sync (push) Failing after 1s
2026-10-14 18:59:16 +03:00
hacker07 c4f7c4f710 v1.2.1.9: kit fix снова выполняется при установке; убрано только описание команды 2026-10-14 18:57:00 +03:00
hacker07 19eb77be68 подпись релиза 1.2.1.8
Синхронизация с автором / sync (push) Failing after 2s
2026-10-14 18:30:20 +03:00
hacker07 333b8edc9a release.sh: проверка меню через python из PATH (python3 есть не везде) 2026-10-14 18:27:05 +03:00
hacker07 13dfb44f3f v1.2.1.8: подсказка меню 0-11, тест меню в гейте, без kit fix и лишних строк в установке 2026-10-14 18:25:33 +03:00
hacker07 3a8518651c подпись релиза 1.2.1.7
Синхронизация с автором / sync (push) Failing after 1s
2026-10-14 18:05:59 +03:00
hacker07 915dd35f42 v1.2.1.7: переставлен порядок — сначала качаем все файлы релиза, потом проверяем установщик 2026-10-14 18:03:57 +03:00
hacker07 edabfd9a83 подпись релиза 1.2.1.6
Синхронизация с автором / sync (push) Failing after 1s
2026-10-14 17:55:19 +03:00
hacker07 741a70c9bf v1.2.1.6: доступы к панели в конце установки; все файлы релиза скачиваются и сверяются (рабочая заглушка) 2026-10-14 17:53:23 +03:00
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
hacker07 6da8650d1d Релиз v1.2.7: подпись (имя 3X-UI_autoVPS, баннер Hacker 07, шапка в двух строках)
Синхронизация с автором / sync (push) Successful in 1s
2026-10-12 21:35:42 +03:00
hacker07 8d470f3eb1 3X-UI_autoVPS: баннер Hacker 07, новое имя вместо 3X-UI KIT, терпимая шапка релиза; -дублирование подсказки SSH; робот без --apply 2026-10-12 21:30:39 +03:00
hacker07 c270b09891 kit help: on/off отдельными строками (проверка справки проходит) 2026-10-12 21:21:43 +03:00
hacker07 a1ce2f1c54 kit help: полный справочник по категориям + проверка, что справка не отстала от команд 2026-10-12 21:21:21 +03:00
hacker07 65f29782fe FORK.md: как работает автоматическая синхронизация на раннере 2026-10-12 21:19:31 +03:00
hacker07 db549348e5 Заглушки: живые кнопки и ссылки с окном «раздел в разработке» 2026-10-12 21:17:55 +03:00
hacker07 7cc7e684d5 v1.2.7: в конце установки — подписка, QR-код и ссылки по протоколам (--no-sub); расписание возвращено на 03:30
Синхронизация с автором / sync (push) Successful in 1s
2026-10-12 21:16:00 +03:00
hacker07 5e316fb1be temp: расписание раз в 5 минут для проверки планировщика
Синхронизация с автором / sync (push) Successful in 1s
2026-10-12 21:11:32 +03:00
hacker07 6b497b047d Робот ежесуточной синхронизации с автором + workflow Gitea Actions (расписание 03:30)
Синхронизация с автором / sync (push) Successful in 2s
2026-10-12 21:10:32 +03:00
hacker07 13c86723be Релиз v1.2.6: подпись
CI / check (push) Canceled after 0s
2026-10-12 20:54:25 +03:00
hacker07 96823d74f1 README переписан целиком (команда, флаги, протоколы, заглушки, команды, канал) + свои картинки 2026-10-12 20:42:07 +03:00
hacker07 ca5a57db55 v1.2.6: заглушка №1 — дата-аналитика вместо хостинга; гейт по переводам строк выше проверки коммита 2026-10-12 20:37:50 +03:00
hacker07 628130992d LF во всех файлах релиза + гейт в release.sh (CR ломает суммы и запуск на сервере) 2026-10-12 20:31:20 +03:00
hacker07 445c066791 v1.2.6: 10 ИТ-сайтов-заглушек вместо авторских + умолчание протоколов REALITY и AWG-3.1 2026-10-12 20:30:14 +03:00
hacker07 1fdc4945f1 v1.2.6: вопрос про смену порта SSH вместо второго порта; меню и вопросы больше не попадают в значения переменных 2026-10-12 20:25:15 +03:00
hacker07 cc17b11bd1 v1.2.6: вопрос о протоколах при установке (+--yes, --harden) и правка разбора ответов 2026-10-12 20:22:17 +03:00
hacker07 4ed9c5ceb5 v1.2.6: обёртка спрашивает про ping, автообновления, порт SSH и пароль 2026-10-12 20:14:07 +03:00
hacker07 0f31fc1739 docs: шапка README с предупреждением о форке + пояснение в FORK.md
CI / check (push) Canceled after 0s
2026-10-12 20:01:01 +03:00
hacker07 619b2120ce docs: FORK.md под новую установку одной командой и все команды kit local
CI / check (push) Canceled after 0s
2026-10-12 19:57:46 +03:00
24 changed files with 4369 additions and 875 deletions
+32
View File
@@ -0,0 +1,32 @@
name: Синхронизация с автором
# Раз в сутки проверяем обновления у автора проекта и, если они есть, переносим наши правки
# на свежий код и публикуем в ветку dev. Релиз и подпись здесь НЕ делаются: закрытый ключ
# подписи лежит только на рабочей станции владельца.
#
# Ручной запуск: в Gitea → Actions → «Синхронизация с автором» → Run workflow.
on:
schedule:
- cron: "30 3 * * *" # каждую ночь в 03:30 по времени сервера Gitea
workflow_dispatch: {}
push:
paths:
- ".gitea/workflows/fork-sync.yml"
concurrency:
group: fork-sync
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Подтянуть обновления автора, проверить и опубликовать
env:
GITEA_URL: http://10.10.10.54:3000
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
curl -fsSL "$GITEA_URL/hacker07/3X-UI_autoVPS/raw/branch/main/tools/fork-sync-bot.sh" -o /tmp/fork-sync-bot.sh
bash /tmp/fork-sync-bot.sh
+401 -6
View File
@@ -1,8 +1,407 @@
## v1.2.1.9: kit fix снова выполняется при установке
* В 1.2.1.8 я понял просьбу слишком широко и **выключил сам `kit fix`** — вместе с ним пропала и
синхронизация времени. Возвращено: `kit fix` при установке выполняется как раньше (время, службы, права),
ключ `--no-time` его пропускает.
* Убрана только та строка, о которой шла речь: описание команды «kit fix – безопасные исправления
(службы, права, автообновление, сертификат, время)» больше не печатается — результат («Чинить нечего»
или список найденного) остаётся.
* Наша строка шага сокращена: «kit fix: безопасные починки (время, службы, права)».
## v1.2.1.8: подсказка меню исправлена, установка стала тише
* **«Выбор [0-10]» при 11 пунктах** — подсказка главного меню отстала, когда мы добавили пункт «Доступ к
панели, ссылки и заглушка». Теперь «Выбор [0-11]». Чтобы это не повторилось, добавлен тест
`tools/check-menus.py`: он сверяет подсказку каждого меню с числом пунктов и запускается в релизном
гейте — если мы снова добавим пункт и забудем подсказку, выпуск остановится.
* **`kit fix` больше не выполняется при установке** (в выводе это выглядело как «Чинить нечего: всё в
порядке»). Нужен — запускается из меню или ключом `--time`.
* **Вывод про автообновления безопасности сокращён до одной строки** («автообновления безопасности
включены…»): три строки про ядро и перезагрузку убраны из установки.
## v1.2.1.7: исправлена установка (моя регрессия в 1.2.1.6)
* В 1.2.1.6 я переписал загрузку файлов релиза и оставил проверку SHA256 установщика **раньше**
скачивания — установка обрывалась на первой же строке: `sha256sum: .../scripts/3x-ui.sh: No such file
or directory`. Теперь сначала скачиваются все файлы релиза (каждый сверяется с подписанным списком),
и только потом проверяется установщик.
* Проверено не на глаз: воспроизвёл ошибку у себя тем же путём (`--verify-only`) — красное, затем после
правки зелёное. Этот прогон добавлен в процедуру выпуска: перед подписью я запускаю установщик в режиме
`--verify-only` и убеждаюсь, что путь «скачать и проверить релиз» проходит целиком.
## v1.2.1.6: доступы к панели в конце установки и рабочая заглушка
* **Исправлено: доступы к панели не выводились.** Автор пишет строки в `/root/3x-ui.txt` без ведущих
пробелов, а наш фильтр требовал два пробела — отсюда «данных панели нет». Теперь адрес панели, логин и
пароль печатаются в конце установки (и в пункте меню «9. Доступ к панели, ссылки и заглушка»).
Рядом указывается, что подписка и ссылки по протоколам лежат в том же файле.
* **Исправлено: наша заглушка не готовилась до установщика.** Обёртка скачивала из релиза только три файла,
а `scripts/kit-local.sh` (там функция страницы) не качала — падало с «kl_site_html: command not found».
Теперь качаются все файлы релиза, и каждый сверяется с подписанным списком (проверка целостности стала
полной, а не только для установщика).
## v1.2.1.5: доступ к панели, ссылки и заглушка — одним пунктом меню
* **В меню kit пункт «9. Доступ к панели, ссылки и заглушка»** — открывает одним экраном: адрес панели,
логин и пароль (из /root/3x-ui.txt), состояние сертификата панели (доверенный на IP или
самоподписанный, с датой повтора при лимите Let's Encrypt), ссылки на подключения с QR-кодом и что
сейчас стоит заглушкой. Раньше доступ к панели в меню не показывался вообще, а про заглушку было
непонятно, как её посмотреть.
* Меню перенумеровано: «10. Панель x-ui», «11. Удалить KIT с сервера» (логика удаления — авторская).
* То же самое доступно из консоли: `kit local access [имя]`.
## v1.2.1.4: короткий итог установки, автовыбор в общедоступных сайтах, команда про порты
* **Итог установки вместо простыни текста** — компактный блок: сборка, сервер, протоколы, состояние
сертификата панели, одна строка проверки. Дальше только то, что нужно человеку: доступ к панели (адрес,
логин, пароль — по просьбе из ранних правок), указатель на меню kit для ссылок и QR, открытые порты и
команда для закрытия лишних. Длинные отчёты (состояние защиты, полный вывод kit check) убраны из
установки — они остались доступны командами, в том числе `kit local hardening`.
* **Ссылки на подключения больше не попадают в вывод установки даже косвенно**: раньше печатался весь
файл /root/3x-ui.txt, а в нём лежат и ссылки vless://. Теперь из него берутся только строки панели —
адрес, логин, пароль.
* **В меню общедоступных сайтов последним пунктом добавлен автовыбор** (раньше можно было только выбрать
номером или вернуться).
* **Новая команда `kit local ports`** — что открыто в ufw, что реально нужно (порты подключений и SSH),
что лишнее; `kit local ports --close-extra` закрывает лишнее.
* Ошибка подготовки нашей заглушки теперь печатает причину, а не просто «не смог».
## v1.2.1.3: ссылки и QR — в меню kit; сертификат панели видно в итоге
* **Ссылки для подключения и QR-код больше не печатаются в выводе установки.** Их смотрят позже, когда
настраивают телефон, и они всегда доступны заново: `kit → Пользователи → Показать ссылки`. В итоговом
блоке вместо них — короткий путь к этому пункту и команды `kit local links` (подписка и QR) и
`kit user link <имя> --all` (все протоколы). Доступы к панели (адрес, логин, пароль) по-прежнему
в конце установки: их вы просили показывать один раз и сразу.
* **Добавлена команда `kit local links`** — подписка, QR-код и ссылки по протоколам одной командой;
пользователя можно указать, иначе берётся первый.
* **В итоговом блоке видно состояние сертификата панели.** Панель и подписка висят на IP, поэтому
печатается: «Let's Encrypt на IP — доверенный, браузер не ругается» либо «самоподписанный» с датой,
после которой Let's Encrypt выпустит сертификат на IP (тот же лимит, что и для домена).
## v1.2.1.2: сертификат на IP — с датой повтора; «без домена» продолжает установку
* **Отказ Let's Encrypt на IP теперь объясняется честно.** Как и для домена, при лимите в выводе дата,
после которой выпуск возможен, и прямо сказано, что порт 80 и файрвол тут не при чём. Раньше печатались
«частые причины» и человек шёл проверять хостера.
* **Пункт «без своего домена» больше не останавливает установку.** При отказе с сертификатом домена выбор
«3» продолжает работу сразу: маскировка берётся из соседей по подсети (или известный сайт, если соседей
нет), панель и подписка переключаются на IP, дальше установка идёт своим ходом. Перезапускать скрипт
и заново отвечать на вопросы не нужно.
## v1.2.1.1: сборка на новой базе автора (3X-UI KIT 1.2.1)
**Правило нумерации:** берём версию автора и через точку добавляем свою, начиная с 1. У автора 1.2.1 —
значит наша сборка 1.2.1.1; когда выйдет его 1.2.2, наша будет 1.2.2.1.
**Взяли от автора (его код теперь работает вместо наших заплаток):**
* проверка A-записи домена сверяется с **публичным DNS**, а не с кэшем самого сервера — та самая причина,
когда запись уже есть, а сервер её «не видит»;
* пропал наш шов, открывавший порт 80: автор делает это сам перед выпуском сертификата **и закрывает обратно**;
* пропал наш шов переиспользования сертификата: автор тоже берёт прошлый;
* повторный запуск после неудачной установки: автор заново спрашивает протоколы и сайт маскировки и не
оставляет сервер на полпути — прежнее наше «доустанавливаю» больше не нужно;
* полное удаление: наш `kit local uninstall` теперь **делегирует авторской команде `kit uninstall`**
(она снимает и сам kit) и остаётся синонимом;
* меню `kit` — авторское (в том числе «9. Панель 3X-UI (меню x-ui)», «10. Удалить KIT с сервера»);
* попутно приехали его починки: Hiddify 4.x снова принимает подписку, ссылка MTProto ведёт на 443, смена
сайта маскировки не обрывает клиентов (прежнее имя живёт неделю), `nginx -t` в проверке сервера,
устойчивость к битому apt-репозиторию, длинные имена сайта в nginx, проверка копии (в т.ч. U+202E),
честная глубокая проверка, предупреждение «обновлять панель и ядро только через kit».
**Сохранили своё:**
* наш файл `scripts/kit-local.sh` и наша обёртка установщика — автор их не трогает;
* наш канал обновлений (GitHub → наш Gitea) и **только наш ключ подписи** в `KIT_SIGNERS`: сервер обязан
принимать обновления лишь из нашего канала;
* наш экран выбора протоколов (13 пунктов, дефолт REALITY + AmneziaWG-3.1) — теперь номера вводятся
**через пробел** («1 12»), как вы просили; запятые по-прежнему принимаются;
* наши меню маскировки REALITY и XHTTP/MTProto, вопрос «панель по IP или по домену», возврат «ввести
другой домен» без обрыва установки;
* наша заглушка ставится сразу, авторская не устанавливается;
* финальный блок: доступы, подписка и QR печатает наш установщик один раз, в самом конце (авторский дубль
«Готово! Сервер работает» убран, предупреждения остаются).
**Технически:** из 77 прежних швов 66 легли без изменений, 8 больше не нужны (их работу делает автор),
3 перенастроены на его новый код (ключ подписи, защита наших меню, финальный блок).
## v1.2.27: ошибка с доменом больше не обрывает установку
* **Свой домен вводится заново, пока не заработает.** Раньше при промахе (A-запись ещё не разошлась
по DNS, опечатка в имени) установка молча маскировалась под чужой сайт, а при домене из флага —
обрывалась с «исправьте причину и запустите скрипт снова». Теперь сразу после ввода домена идёт
проверка и при промахе — выбор: «проверить снова» (DNS расходится от минут до часов), «ввести другой
домен», «без своего домена — маскировка соседом по подсети».
* **Провал сертификата на шаге подключений не прерывает установку.** Предлагается повторить попытку
(пока ждали обновления DNS или лимит Let's Encrypt), ввести другое имя (с той же проверкой A-записи)
или остановиться с понятной инструкцией, как поставить без домена. Причина и дата повтора при лимите
печатаются в самом отказе, лог остаётся в /var/log/kit-domain-cert.log.
## v1.2.26: лимит Let's Encrypt виден прямо, готовый сертификат переиспользуется
* **Причина отказа теперь называется честно.** 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 и находит свои службы сам
* В списке сноса не было `kit-limit.service` и `kit-limit.timer` (юнит «общий лимит трафика» из kit) —
его создаёт установка, а `kit local uninstall` его не убирал. Из-за этого после сноса оставался
работающий таймер, который раз в 5 минут дёргал уже удалённый `kit` и падал.
* Теперь снос берёт список служб **и из явного перечня, и по шаблонам `x-ui*`/`kit*`** из systemd —
новая служба, добавленная автором позже, не останется незамеченной. Проверка после сноса ищет
по тем же шаблонам и показывает всё найденное.
* Флаг `--clean` у установщика использует тот же список.
## v1.2.10: убран мусор в выводе установщика
* В шапке `scripts/3x-ui-local.sh` один из флагов потерял `#` и **при каждом запуске печаталось
`--dry-run: command not found`** (до `set -e` — потому и не роняло установку, но выглядело как ошибка).
Исправлено, заодно выровнены строки справки.
* В релизный гейт добавлена проверка, которая такое ловит: холостой прогон установщика, чей вывод в stderr
содержит «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, живые кнопки на заглушках
* **Сборка называется `3X-UI_autoVPS`** (было `3X-UI KIT`): баннер сервера — `Hacker 07` в том же
стиле, новое имя в справке, версии, описаниях служб и в имени профиля в клиенте. В файлах автора
это сделано швами (`tools/fork_apply.py`), поэтому обновление автора правки не затирает.
* **Совместимость шапки релиза.** В подписанном `SHA256SUMS` этого релиза шапка пишется **двумя
строками** (старая и новая), а проверка на сервере принимает обе. Иначе уже установленные серверы
отказались бы ставить обновление: они сверяют точный вид строки. Со следующего релиза останется
только новое имя.
* Убрано дублирование подсказки «Дальше обязательно» при смене порта SSH: она печаталась и
установщиком, и командой `kit local ssh`.
* **`kit help` — полный справочник по категориям**: пользователи, подключения и порты, маскировка и
сайт, проверка и починка, обслуживание и обновление, защита сервера (наш слой). Раньше `kit help`
показывал короткий список автора и не упоминал наши команды. Чтобы справка не отставала молча,
добавлена проверка `tools/check-help.sh`: она сверяет справку с диспетчером команд и встроена в
сборку релиза (`tools/release.sh`).
* У сайтов-заглушек все ссылки и кнопки **живые**: нажатие открывает окно «раздел в разработке,
приносим извинения» (закрыть — кнопкой, кликом мимо окна или Esc). Никаких внешних запросов:
скрипт и стили внутри страницы.
* В самом конце установки (после всех проверок) печатается то, что нужно сохранить: **подписка
с QR-кодом** и ссылки по протоколам — авторской командой `kit user link <имя> --all`, поэтому
вывод совпадает с тем, что показывает `kit` потом. Отключается флагом `--no-sub`.
* Набор по умолчанию — **REALITY + AmneziaWG-3.1** (самый совместимый минимум); ответ «все»
ставит полный набор автора. Умолчание задаётся в одном месте (`DEFAULT_PROTOS`).
* **Сайт-заглушка переделан на ИТ-тематику:** десять правдоподобных сайтов (дата-аналитика, DevOps,
мониторинг, разработка, аудит, ИТ-поддержка, колокейшн, заметки про Linux, бэкапы, документация)
вместо авторских «кофеен и пекарен». Ставится случайный, конкретный — `kit local site 7`,
список — `kit local site --list`. Команда обновляет и файл для nginx, и копию страницы внутри
подключения Hysteria2 (`kit net masq on`): если менять файл вручную, 443/tcp и UDP-порт начнут
отвечать разными страницами — это заметно снаружи.
* Обёртка спрашивает, **какие протоколы ставить** (нумерованный список, `1,3,11` или «все»,
Enter — набор автора): невыбранные протоколы не создаются вообще — ни подключения в панели,
ни записей в nginx, ни ссылок в подписке. Список и умолчание взяты из самого установщика
(`ALL_PROTOS`/`DEFAULT_PROTOS`), передаётся его же флагом `--protocols`, так что поведение
не расходится с авторским. Свой `--protocols` можно задать флагом — тогда вопроса не будет.
* Обёртка `scripts/3x-ui-local.sh` теперь спрашивает, а не решает за вас: закрывать ли ответ на ping,
включать ли автообновления безопасности, **менять ли порт SSH** (Enter — оставить текущий,
число — перевести на него; мусор и занятый порт переспрашивает), менять ли пароль root.
Маскировку и домен, как и раньше, спрашивает установщик автора.
* Смена порта SSH никогда не закрывает прежний порт сама: сначала добавляется новый, затем скрипт
печатает две команды — проверить вход во втором окне и закрыть прежние (`kit local ssh --close-old`).
* Запуск без флагов = «одна команда, дальше вопросы». Флаг `--yes` — наоборот, ничего не спрашивать
(для автоматизации): ping закрывается, автообновления включаются, порт SSH и пароль не трогаются.
* Флаг `--harden` теперь действительно существует (раньше он уходил установщику автора неизвестным
параметром и ронял установку): закрыть ping и включить автообновления без вопросов.
* `--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` пишет суммы как
@@ -13,14 +412,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: проверка сервера → скачивание нашего релиза → проверка подписи
@@ -35,7 +432,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` (закрыть прежние
@@ -52,8 +448,7 @@
а закрытие старых портов не делается автоматически — там ошибка запирает доступ к серверу. а закрытие старых портов не делается автоматически — там ошибка запирает доступ к серверу.
* 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 KIT 1.2
* Источник файлов и обновлений — свой репозиторий Gitea: установка и `kit update` берут релиз * Источник файлов и обновлений — свой репозиторий Gitea: установка и `kit update` берут релиз
оттуда, а подпись релиза проверяется ключом форка. Авторский ночной апдейт приходит в наш оттуда, а подпись релиза проверяется ключом форка. Авторский ночной апдейт приходит в наш
@@ -65,4 +460,4 @@
* Швы в коде автора описаны в `tools/fork_apply.py` (источник правды), синхронизация с автором и * Швы в коде автора описаны в `tools/fork_apply.py` (источник правды), синхронизация с автором и
проверки — `tools/fork-sync.sh`. проверки — `tools/fork-sync.sh`.
Основа — 3X-UI KIT 1.2 (панель 3X-UI v3.9.0, ядро Xray v26.6.27). Что нового у автора — в `CHANGELOG.md`. Основа — 3X-UI_autoVPS 1.2 (панель 3X-UI v3.9.0, ядро Xray v26.6.27). Что нового у автора — в `CHANGELOG.md`.
+224 -74
View File
@@ -1,108 +1,258 @@
# Форк 3X-UI KIT (hacker07) # Форк 3X-UI_autoVPS (hacker07) — своя сборка и свой канал обновлений
Это личный форк [3X-UI KIT](https://github.com/itsnotkubrick/3X-UI_KIT): наш код живёт рядом с Коротко: это [3X-UI_autoVPS](https://github.com/itsnotkubrick/3X-UI_KIT) со своими доработками. Наш код
кодом автора и не ломается, когда автор обновляет свой. Здесь описано, как это устроено и что живёт рядом с кодом автора и не ломается, когда автор выпускает новую версию, а серверы ставятся и
делать после каждого обновления автора. обновляются **из этого репозитория** — релизы подписываются нашим ключом.
## С чего начать: новый VPS
1. Если нужен свой домен для маскировки — заранее направьте его A-запись на IP сервера и освободите
порт 80 (иначе Let's Encrypt не выпустит сертификат). Без домена этот шаг пропускается.
2. Одна команда на сервере (от root) — **дальше скрипт спросит сам**: какие протоколы ставить
(Enter — REALITY + AmneziaWG-3.1, «все» — набор автора), какую маскировку и домен,
закрывать ли ответ на ping, включать ли автообновления, менять ли порт SSH (Enter — оставить
текущий, число — перевести на него), менять ли пароль root:
```bash
bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui-local.sh)
```
Те же ответы можно задать флагами — тогда вопросов не будет:
```bash
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --protocols reality,hy2,awg,mtproto --domain vpn.example.com
```
3. Скрипт делает всё сам: проверяет сервер → скачивает наш релиз и **проверяет подпись `SHA256SUMS`
и SHA256 установщика** → запускает установщик автора (панель 3X-UI, **только выбранные протоколы**,
сайт-заглушка, маскировка, сертификат, подписка, ufw) → наши вопросы и правки (ping,
автообновления, второй порт SSH, пароль root) → `kit fix` → в конце показывает `kit check` и сводку
`kit local hardening`.
4. Забрать ссылки и подписку: `kit user link admin` (или адрес подписки из вывода установки).
Сомневаетесь в канале — сначала ничего не меняющие режимы:
```bash
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --verify-only # только проверка подписи релиза
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --dry-run --domain vpn.example.com # план установки
```
### Флаги установки
| Флаг | Что делает |
|---|---|
| `--protocols список` | какие протоколы ставить: `all`, `minimal` или через запятую — `reality,reality2,hy2,xhttp,ws,trojan,vmess,ss,tuic,wg,awg,awg3,mtproto`. Невыбранные не создаются вообще. Без флага — вопрос, по умолчанию предлагаются **REALITY + AmneziaWG-3.1** (ответ «все» — набор автора) |
| `--site N\|random\|off` | сайт-заглушка: конкретный из нашего набора (10 ИТ-сайтов), случайный (по умолчанию) или не трогать авторскую страницу (то же — `--no-site`) |
| `--yes` | не задавать вопросы (ping закрывается, автообновления включаются, порт SSH и пароль не трогаются) |
| `--harden` | закрыть ответ на ping и включить автообновления без вопросов |
| `--domain домен` | свой домен для маскировки: сайт и REALITY маскируются под него (A-запись на сервер, свободный 80) |
| `--sni сайт.com` | маскировка под чужой сайт вместо домена |
| `--panel-on domain\|ip` | где открывать панель и подписку |
| `--ssh-port N` | перевести SSH на порт N (число 1024–65535). Прежний порт сам не закрывается: сначала проверьте вход во втором окне, потом `kit local ssh --close-old N` |
| `--no-harden` | не закрывать ответ на ping и не включать автообновления безопасности |
| `--no-time` / `--no-check` | не запускать `kit fix` / `kit check` |
| `--verify-only` / `--dry-run` | проверить релиз / показать план и ничего не менять |
| остальные | передаются установщику автора как есть: `--host`, `--user`, `--port`, `--protocols`, `--restore`, `--multi-port`, `--cert`/`--key`, `--panel-ssl` |
Чего скрипт **не** делает (сознательно): `apt full-upgrade` (тянет систему вперёд и может сломать
закреплённые панель 3X-UI 3.9.0 и ядро Xray 26.6.27 — вместо него обновления безопасности),
автоматическую перезагрузку и смену пароля root (в однострочник пароль не подставляем, он попал бы
в список процессов — для этого есть `kit local passwd`).
### Как проверить, что установка удалась
```bash
kit check # без ❌: службы, порт 443 (nginx), подписка, маскировка, сертификат
kit local hardening # сводка: порты SSH, ответ на ping: deny, ufw активен, автообновления: включены
kit local channel # источник — этот репозиторий, ключей в KIT_SIGNERS: 1 (наш)
kit local version # версия сборки: 1.2.5 или новее
ping <IP сервера> # с домашней машины — таймаут: ответ на ping закрыт, это ожидаемо
kit user link admin # ссылка подписки; откройте её в браузере
```
Если ставили с доменом, в `kit check` должны быть строки «свой домен … указывает на этот сервер» и
«сайт маскировки … отвечает по TLS 1.3 и HTTP/2»; предупреждения про известный сайт на чужом IP
быть не должно (это касалось бы вспомогательного XHTTP, у REALITY должен быть ваш домен).
## Команды форка (`kit local`)
```bash
kit local help # справка по всем командам форка
kit local version # версия, источник обновлений, дата сборки нашего файла
kit local channel # откуда берутся обновления и чем проверяется подпись
kit local seams # какие швы форка сейчас в установленном /usr/local/bin/kit
kit local masking # состояние маскировки (у REALITY должен быть свой домен)
kit local hardening # сводка по защите сервера
```
Защита сервера (поведение взято из проекта `StressOzz/vps-setup`, реализация своя и безопаснее):
| Команда | Что делает |
|---|---|
| `kit local site` | поставить случайный сайт-заглушку из нашего набора (10 ИТ-сайтов) и синхронизировать ответ Hysteria2 |
| `kit local site --list` / `kit local site 7` / `--show` | список сайтов с номерами / поставить конкретный / какой стоит сейчас |
| `kit local ssh 2222` | добавить порт SSH (прежние продолжают работать; порт сначала открывается в ufw, конфиг проверяется через `sshd -t`) |
| `kit local ssh --status` | какие порты SSH слушаются сейчас |
| `kit local ssh --close-old 2222` | закрыть прежние порты после того, как новый проверен из второго окна |
| `kit local ping off\|on\|status` | ответ на ICMP echo: закрыть/вернуть/показать (правилом ufw, видно в `ufw status`) |
| `kit local passwd [юзер]` | сменить пароль (спросит дважды, в историю команд не попадёт) |
| `kit local passwd --generate` | сгенерировать пароль и показать один раз |
| `kit local upgrades on\|off\|status` | автообновления безопасности (только security, без перезагрузки) |
К любой правке можно добавить `--dry-run` — покажет план и ничего не изменит. Копии изменённых
файлов складываются в `/root/kit-local-backup-<дата>/` (не в `/tmp`).
Что `ping off` затрагивает: только ICMP echo. Подписка (HTTPS), панель, REALITY/XHTTP (TCP 443),
Hysteria2 (UDP 443) работают как работали; исходящий ping с самого сервера тоже остаётся. Если
сервер есть в вашем мониторинге — пробу до него делайте по HTTPS/TCP, а не по ICMP.
## Что где лежит ## Что где лежит
| Что | Где | Правит | | Что | Где | Правит |
|---|---|---| |---|---|---|
| Наши доработки (команды, хуки) | `scripts/kit-local.sh` | только мы | | Наши доработки (команды, хуки) | `scripts/kit-local.sh` | только мы |
| Установка одной командой (обёртка) | `scripts/3x-ui-local.sh` | только мы |
| Описание «швов» в коде автора | `tools/fork_apply.py` | только мы | | Описание «швов» в коде автора | `tools/fork_apply.py` | только мы |
| Синхронизация с автором и проверки | `tools/fork-sync.sh` | только мы | | Синхронизация с автором и проверки | `tools/fork-sync.sh` | только мы |
| Записи о наших версиях | `FORK-CHANGELOG.md` | только мы |
| `README.md` | целиком, включая картинки в `manuals/assets/` | только мы |
| Файлы автора (`scripts/kit.sh`, `scripts/3x-ui.sh`, …) | как есть | только автор | | Файлы автора (`scripts/kit.sh`, `scripts/3x-ui.sh`, …) | как есть | только автор |
Ключевая идея: **в файлах автора мы правим только «швы»** — несколько констант и одну строку Ключевая идея: в файлах автора мы правим только «швы» — несколько констант, одну строку подключения
подключения. Всё, что мы дописываем по существу, лежит в нашем файле, которого автор не знает. и нормализацию сумм в релизном скрипте. Всё, что мы дописываем по существу, лежит в наших файлах.
Швы описаны в одном месте (`tools/fork_apply.py`) и накатываются только на чистое дерево автора; Швы описаны в одном месте (`tools/fork_apply.py`) и накатываются только на чистое дерево автора;
если автор изменил нужную строку — инструмент останавливается и говорит об этом, а не делает если автор изменил нужную строку — инструмент останавливается и говорит об этом, а не делает
полу-патч. полу-патч.
## Ветки ## Ветки
`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` + швы + наши файлы; из неё делаются релизы.
- релизы — теги `v<номера>`; номер = `<версия автора>.<наш уровень>` (например, автор 1.2 → наш - `main` — то же состояние плюс документация после релиза (по нему считается «последняя версия»).
первый релиз 1.2.1, следующий наш — 1.2.2, после релиза автора 1.3 — 1.3.1). - релизы — теги `v<номера>`; номер = `<версия автора>.<наш уровень>` (автор 1.2 → наш 1.2.1; после
релиза автора 1.3 → 1.3.1). Номер обязан строго расти: `kit update` не принимает откат назад.
Номер обязан строго расти: на серверах `kit update` не принимает откат назад. ## Обновление сервера и канал
## Обновление автора (по шагам) ```bash
kit update --force && kit check # тянет наш релиз из этого репозитория, проверяет нашу подпись
```
Источник файлов — `KIT_RAW` / `kit_ref_raw()` в `scripts/kit.sh` (наш Gitea и наш тег), подпись
проверяется ключом из `KIT_SIGNERS` (там только ключ форка). Ночной `kit-update.timer` (03:00) делает
то же автоматически — поэтому сервер и должен смотреть в этот репозиторий: иначе наши доработки
исчезнут в первую же ночь.
Вернуться на канал автора: скачать `scripts/kit.sh` из релиза автора с GitHub в `/usr/local/bin/kit`,
затем `kit update --force`.
## Обновление от автора (с рабочей станции)
### Автоматически: робот на раннере (CT 114)
Раз в сутки (03:30) Gitea сама запускает `.gitea/workflows/fork-sync.yml` на раннере в CT 114:
робот `tools/fork-sync-bot.sh` забирает ветку автора, переносит на неё наши правки, прогоняет те же
проверки, что и `tools/fork-sync.sh`, дописывает раздел в `FORK-CHANGELOG.md` и публикует `dev`
(`--force-with-lease`). Не сошлось (конфликт в местах наших правок) — **ничего не публикуется**,
причина видна в Gitea → Actions и в `/opt/kit-fork-sync/last-run.log` на контейнере.
Робот **не** выпускает и не подписывает релиз: закрытый ключ подписи остаётся на рабочей станции.
После автоматической синхронизации — как обычно: `git fetch origin && git checkout dev && git reset --hard origin/dev`
(робот переписывает `dev`), описать изменения в `FORK-CHANGELOG.md`, затем `tools/release.sh` и тег.
```bash ```bash
cd H:/My_Proj/Fork_3X-UI_KIT cd H:/My_Proj/Fork_3X-UI_KIT
bash tools/fork-sync.sh # подтянуть автора, перенести наши правки, прогнать проверки 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 + подпись нашим ключом
git add -A && git commit -m "Релиз v$(cat VERSION): швы форка + подпись"
git tag v$(cat VERSION) **Перед подписью — обязательный прогон.** `bash scripts/3x-ui-local.sh --verify-only` проверяет ровно тот
путь установки, которым пойдут серверы: скачивание всех файлов релиза и сверка каждого с подписанным
списком. На Windows запускать с `TMPDIR=C:/…` (нативные утилиты не понимают MSYS-пути).
**Нумерация версий.** Основа — версия автора, наша надстройка добавляется через точку с единицы:
у автора 1.2.1 → наша сборка 1.2.1.1; вышла его 1.2.2 → наша 1.2.2.1. Так на сервере всегда видно,
на чьей базе собрана сборка.
**Обновление базы автора.** Когда автор выпускает новую версию: перенести его файлы
(`git checkout upstream -- scripts/kit.sh scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit-sub.py`),
прогнать `python tools/fork_apply.py` и посмотреть, какие якоря не нашли себя. Те, чью работу автор уже
делает сам, — вычеркнуть; остальные перенастроить на его новый код и пересобрать таблицу швов
`git branch -f upstream <коммит автора>`.
Скрипт сам приводит переводы строк к LF, проверяет справку и то, что установщик не ругается на пустые
строки, сверяет панель и ядро Xray с официальными хешами GitHub (если GitHub недоступен — предупреждает и
пропускает эти проверки за секунды, а не висит; расхождение хешей при живом GitHub по-прежнему останавливает
выпуск). Далее скрипт ждёт **парольную фразу ключа** — её вводит человек, поэтому запускать надо в окне, где
можно набрать пароль (в PowerShell — да, в фоне — нет).
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
``` ```
## Установка и обновление из этого репозитория
```bash
# установка одной командой: наш релиз, подпись проверяется до запуска установщика,
# в конце — закрытие ping, автообновления безопасности, kit fix и kit check
bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui-local.sh) --domain vpn.example.com
# «а что вы вообще собираетесь запускать?» — проверка подписи релиза без установки
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --verify-only
# план установки без изменений
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --dry-run --domain vpn.example.com
# то же, но без защиты (--no-harden) и с добавлением второго порта SSH
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --sni dl.google.com --no-harden --ssh-port 2222
# только установщик автора, без наших дополнений
bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui.sh)
# обновление уже стоящего сервера (тянет наш релиз и проверяет нашу подпись)
kit update --force && kit check
```
Внутри `kit` источником файлов служит `KIT_RAW`/`kit_ref_raw()` — они указывают на этот
репозиторий, а подпись релиза проверяется ключом из `KIT_SIGNERS` (только ключ форка).
## Подпись релизов ## Подпись релизов
- Открытая часть ключа — `tools/fork-signer.pub` и в `KIT_SIGNERS` (в `scripts/kit.sh`). - Открытая часть ключа — `tools/fork-signer.pub` и в `KIT_SIGNERS` (в `scripts/kit.sh`); она же
- Закрытая часть — только на рабочей станции владельца (`~/.ssh/3x-ui-kit-fork/release_key`), вшита в `scripts/3x-ui-local.sh`, чтобы проверять подпись до запуска установщика.
с паролем; в репозиторий, CI и на серверы не попадает. Кто получит ключ — получит root на всех - Закрытая часть — только на рабочей станции владельца (`~/.ssh/3x-ui-kit-fork/release_key`).
серверах, которые обновляются из этого репозитория. В репозиторий, CI и на серверы не попадает: кто получит ключ — получит root на всех серверах,
- Смена ключа: подпиши новый релиз старым ключом, иначе серверы его не примут. которые обновляются из этого репозитория. Пароль на ключ пока не установлен
(`ssh-keygen -p -f ~/.ssh/3x-ui-kit-fork/release_key`).
## Наши команды - Смена ключа: новый релиз подписывается **старым** ключом, иначе серверы его не примут.
```bash
kit local help # справка по дополнениям форка
kit local version # версия, источник обновлений, дата сборки нашего файла
kit local channel # откуда берутся обновления и чем проверяется подпись
kit local seams # какие швы форка сейчас в установленном /usr/local/bin/kit
kit local masking # состояние маскировки
kit local hardening # сводка по защите сервера
# защита сервера (поведение из StressOzz/vps-setup, сделано безопаснее)
kit local ssh 2222 # добавить порт SSH (прежние продолжают работать)
kit local ssh --close-old 2222 # закрыть прежние порты после проверки
kit local ping off # не отвечать на ping (правилом ufw)
kit local upgrades on # автообновления безопасности
kit local passwd --generate # сменить пароль root и показать его один раз
```
У любой правки можно добавить `--dry-run` — покажет план и ничего не изменит.
## Грабли, о которых стоит помнить ## Грабли, о которых стоит помнить
1. **Ночное автообновление** (`kit-update.timer`, 03:00) перезаписывает `/usr/local/bin/kit` 1. **Ночное автообновление** (`kit-update.timer`, 03:00) перезаписывает `/usr/local/bin/kit` тем, что
подписанным релизом из настроенного репозитория. Поэтому канал обновления и швы — один шаг: лежит в настроенном репозитории. Канал и наши правки — один шаг, а не два.
сервер должен смотреть в этот репозиторий, иначе наши правки исчезнут ночью. 2. **Переводы строк.** Релиз подписывается по суммам рабочей копии, а сервер сверяет их с файлами из
2. **Переводы строк.** Релиз подписывается по контрольным суммам файлов в рабочей копии, а сервер Gitea: при `core.autocrlf=true` суммы не сойдутся (CRLF против LF). В клоне должно быть
сверяет их с файлами из Gitea. При `core.autocrlf=true` суммы не сойдутся (CRLF против LF): `git config core.autocrlf false` (и `.gitattributes`), проверка есть в `tools/fork-sync.sh`.
в этом клоне должно быть `git config core.autocrlf false`. 3. **Наш файл обязан быть в двух списках** — `FILES=(…)` в `tools/release.sh` и список загрузки
3. **Наш файл обязан быть в списке релиза** (`tools/release.sh`, `FILES=(…)`) и в списке загрузки `fetch_release()` в `scripts/kit.sh`, иначе после обновления `kit-local.sh` останется старым.
`fetch_release()` — иначе при обновлении `kit.sh` приедет новый, а `kit-local.sh` останется старым. 4. **Панель 3X-UI ставится с GitHub** по закреплённой версии и SHA256 — это шаг автора; для установки
4. **Панель 3X-UI ставится с GitHub** по закреплённой версии и SHA256 — это шаг автора, форк его не с нуля серверу нужен доступ к GitHub.
трогает. Для установки с нуля серверу нужен доступ к GitHub. 5. **Формат контрольных сумм.** `kit` сверяет имя файла в `SHA256SUMS` строго (`$2 == "путь"`).
На Windows `sha256sum` пишет `hash *путь`, и такие релизы `kit update` отклоняет («в подписанном
списке нет такого файла»), хотя `sha256sum -c` их принимает. Поэтому `tools/release.sh` приводит
список к POSIX-виду **до** подписи, а релизы **v1.2.1–v1.2.4 ставить нельзя** — только v1.2.5+.
Проверять релиз надо логикой самого `kit`, а не одной `sha256sum -c`.
6. **Сборка релиза обращается к GitHub** (сверка закреплённых версий панели и ядра). Из домашней сети
TLS до GitHub иногда рвётся (`schannel: failed to receive handshake`) — просто повторить запуск.
7. **`README.md` — наш файл целиком.** Авторское описание убрано: иначе человек, открывший
репозиторий, читает команду установки **авторской** сборки, ставит по ней и оказывается на
авторском канале обновлений — ночное автообновление вернёт его сборку. При обновлении из
авторского проекта README конфликтует: берём нашу версию (`git checkout --ours README.md`),
авторские правки в нём нас не интересуют.
## Что проверено на сегодня
- Релиз `v1.2.5`: подпись сходится с ключом из опубликованного `kit.sh`, строгая сверка сумм
`$2 == путь` для `kit.sh`, `kit-sub.py`, `kit-local.sh` — как это делает `kit` на сервере.
- Публичный однострочник в режиме `--verify-only` отвечает «подпись сошлась, SHA256 установщика
совпал» — проверено снаружи, из домашней сети.
- Установка «в бою» на живом сервере пока не прогонялась: сначала её стоит выполнить на новом VPS,
посмотрев `--verify-only` и `--dry-run`.
+188 -282
View File
@@ -1,311 +1,217 @@
<div align="center"> <div align="center">
<img alt="3X-UI KIT" src="manuals/assets/banner.svg" width="820"> <img alt="3X-UI autoVPS" src="manuals/assets/fork-banner.svg" width="860">
**Установка 3X-UI одной командой: 11 протоколов, один порт 443, одна подписка на всё** **Сервер одной командой: панель 3X-UI, VPN-протоколы за одним портом 443, одна подписка на все устройства**
[![Версия](https://img.shields.io/github/v/release/itsnotkubrick/3X-UI_KIT?label=3X-UI%20KIT&color=93E06F&labelColor=221B17)](https://github.com/itsnotkubrick/3X-UI_KIT/releases/latest) Сборка со **своим каналом обновлений**: сервер ставится и обновляется из этого репозитория, релизы
[![Протоколов](https://img.shields.io/badge/протоколов-11-93E06F?labelColor=221B17)](manuals/3x-ui.md#протоколы) подписаны, ночное автообновление не вернёт чужую версию. В основе — открытый проект 3X-UI_autoVPS.
[![3X-UI](https://img.shields.io/badge/3X--UI-v3.9.0-93E06F?labelColor=221B17)](https://github.com/MHSanaei/3x-ui)
[![Звёзды](https://img.shields.io/github/stars/itsnotkubrick/3X-UI_KIT?label=звёзды&style=flat&color=93E06F&labelColor=221B17)](https://github.com/itsnotkubrick/3X-UI_KIT/stargazers)
[![Обновлено](https://img.shields.io/github/last-commit/itsnotkubrick/3X-UI_KIT?label=обновлено&color=93E06F&labelColor=221B17)](https://github.com/itsnotkubrick/3X-UI_KIT/commits)
</div> </div>
--- ## Что ставится
> [!WARNING] | Что | Зачем |
> **Проект – набор скриптов с открытым кодом.** Он не оказывает услуг связи и не предоставляет доступ |---|---|
> к серверам. Вы ставите его на собственный сервер и сами отвечаете за соблюдение законодательства | Панель **3X-UI** + ядро **Xray** | управление подключениями и пользователями |
> своей страны. Проект создан в образовательных целях. | **nginx** на порту 443 | разводит подключения по именам и путям, отдаёт сайт-заглушку |
| VPN-подключения | REALITY, Hysteria2, AmneziaWG, XHTTP, VLESS-WS, Trojan-gRPC, VMess-WS, Shadowsocks, TUIC, MTProto, WireGuard — какие выберете |
> [!TIP] | **Подписка** (`kit-sub`) | одна ссылка на все устройства: Happ, Hiddify, v2rayN, Clash/Mihomo |
> **Вышла версия 1.2:** панель 3X-UI v3.9.0, меню `kit`, раздельная маршрутизация и `--restore`. | **Сайт-заглушка** | на 443 и в ответе Hysteria2 — правдоподобный ИТ-сайт из нашего набора |
> [Что нового](https://github.com/itsnotkubrick/3X-UI_KIT/releases/tag/v1.2) · | **ufw**, обновления безопасности, синхронизация времени | защита сервера и корректная работа REALITY |
> сервер на 1.1.x обновится сам этой ночью, а на 1.0 одной командой:
> ```bash
> curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.2/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update
> ```
**3X-UI KIT** превращает чистый VPS в готовый VPN-сервер за несколько минут. Скрипт ставит
официальную панель [3X-UI](https://github.com/MHSanaei/3x-ui), настраивает все популярные
протоколы, сертификат и файрвол и выдаёт одну ссылку-подписку. Её можно вставить в любое
приложение – оно само получит подходящие ему протоколы. Домен не нужен.
<div align="center">
<img alt="Конец установки 3X-UI KIT" src="manuals/assets/script-3x-ui.svg" width="760">
</div>
## Возможности
- 🧩 **11 протоколов на одном порту 443.** Панель, подписка и все TCP-протоколы спрятаны за одним портом,
а на случайный заход сервер показывает обычный сайт. Протоколы: VLESS REALITY, VLESS XHTTP + REALITY,
VLESS WebSocket + TLS, Trojan gRPC + TLS, VMess WebSocket + TLS, Shadowsocks 2022, Hysteria2, TUIC v5,
AmneziaWG, AmneziaWG 3.1, MTProto (прокси для Telegram).
- 🔗 **Одна подписка на все приложения.** Hiddify, Happ, v2rayN, Karing, Clash Verge и FlClash
получают свой формат и только те протоколы, которые распознают.
- 🧭 **Раздельная маршрутизация.** Через VPN идут только зарубежные сервисы, остальное – напрямую.
Правила приезжают вместе с подпиской, DNS не течёт.
- 🥷 **Маскировка.** Со своим доменом на сервере стоит ваш сайт, без него сайт подбирается среди соседей
по подсети. REALITY работает с `xtls-rprx-vision`.
- 👥 **Пользователи во все протоколы сразу.** Один пользователь – одна подписка, общий лимит трафика,
срок и число устройств. Меню `kit` работает как у x-ui: выбрали цифру – готово.
- 💾 **Резервная копия и переезд на новый сервер одной командой.** Копия сохраняет подключения, ключи
и пользователей, а на чистом VPS (даже с другим IP) сервер поднимается из неё за несколько минут.
- 🔄 **Обновляется само, но только подписанными релизами.**
- 🩺 **Проверка и починка сервера** одной командой.
## Что понадобится
- VPS с **Ubuntu 22.04/24.04** или **Debian 12/13** и доступом root по SSH
- Свободные порты **443** и **80** – на свежем сервере они свободны
Нужен VPS? Я сам использую **[IS Hosting](https://ishosting.io/affiliate/NTg4MiM4)** и рекомендую его
(реферальная ссылка).
## Установка ## Установка
Подключитесь к серверу по SSH и выполните одну команду на чистом VPS. От root, на чистом сервере (Debian 11+ / Ubuntu 22.04+, 64-бит):
Панель 3X-UI, все протоколы на порту 443 и одна подписка на всё:
```bash ```bash
bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh) bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui-local.sh)
``` ```
Через пару минут скрипт покажет адрес панели, логин, пароль и подписку с QR-кодом. Без флагов скрипт **спрашивает всё заранее, до установки**: какие протоколы ставить (Enter — REALITY +
Подробно – подключение приложений, дополнительные пользователи и параметры – в **[инструкции](manuals/3x-ui.md)**. AmneziaWG-3.1), менять ли порт SSH (Enter — оставить 22), менять ли пароль root, закрывать ли ответ на
ping, включать ли автообновления безопасности. Затем сканирует подсеть и **предлагает выбрать сайт
маскировки** — отдельно для REALITY и, если выбраны XHTTP/MTProto, отдельно для них: номер из найденных
соседей, `0` — ввести адрес вручную, для XHTTP/MTProto ещё и известный сайт (Google, Microsoft, AMD…).
## Команды KIT После установщика скрипт **ничего не спрашивает** — только выполняет выбранное, проверяет сервер и в
самом конце показывает то, что нужно унести: **доступ к панели 3X-UI** (адрес, логин, пароль) и
**подписку с QR-кодом** и ссылками по протоколам.
После установки на сервере появляется команда `kit`. Запустите её **без аргументов** – откроется меню, как у `x-ui`: выберите цифру, остальное спросит само. Те же действия одной строкой – в группах ниже (нажмите, чтобы раскрыть). Подробности – в [инструкции](manuals/3x-ui.md). <img alt="Установка одной командой" src="manuals/assets/script-3x-ui-local.svg" width="860">
<details> Если вопросы не нужны — те же ответы задаются флагами:
<summary><b>Пользователи</b> – <code>kit user</code></summary>
```bash ```bash
kit user add имя [--gb 50] [--days 30] [--devices 3] # добавить во все протоколы, показать подписку # минимум: свой домен и два протокола, без вопросов
kit user list # кто сколько израсходовал, до какого числа bash <(curl -fsSL …/scripts/3x-ui-local.sh) --protocols reality,awg3 --domain vpn.example.com
kit user link имя [--all | --amnezia | --telegram] # подписка и ссылки «открыть в приложении»
kit user limit имя [--gb N] [--days N] [--devices N] # изменить лимиты (0 – без ограничений)
kit user off имя / kit user on имя # выключить и включить
kit user del имя # удалить
```
</details>
<details> # всё, кроме WireGuard, и второй порт SSH
<summary><b>Сеть и маскировка</b> – <code>kit net</code></summary> bash <(curl -fsSL …/scripts/3x-ui-local.sh) --protocols все --ssh-port 2222
# ничего не спрашивать: ping закрыть, автообновления включить, порт и пароль не трогать
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --yes
```
Проверить канал, ничего не меняя:
```bash ```bash
kit net # протоколы, порты, сайт маскировки, отпечаток – одним экраном bash <(curl -fsSL …/scripts/3x-ui-local.sh) --verify-only # подпись релиза и сумма установщика
kit net site [сайт.com] # сменить сайт маскировки (без аргумента – среди соседей по подсети) bash <(curl -fsSL …/scripts/3x-ui-local.sh) --dry-run # что именно будет сделано
kit net port имя порт # сменить порт протокола
kit net off имя / kit net on имя # выключить и включить протокол (kit net on reality2 – второй REALITY)
kit net fp firefox # сменить отпечаток клиента у всех подключений
kit net masq on|off # Hysteria2 отвечает на чужой запрос страницей сайта
kit net dns on|off # DNS в подписке по DoH через прокси
kit net panel domain | ip # панель и подписка по своему домену или по IP
```
</details>
<details>
<summary><b>Раздельная маршрутизация</b> – <code>kit net split</code></summary>
Через VPN идёт только список зарубежных сервисов, остальное – напрямую. Подробнее – [в инструкции](manuals/3x-ui.md#раздельная-маршрутизация-kit-net-split).
```bash
kit net split on # создать список и включить
kit net split check # проверить список
kit net split apply # после правки списка обновить профили Happ и Xray
kit net split off # выключить
```
</details>
<details>
<summary><b>Обслуживание сервера</b> – проверка, обновление, копия</summary>
```bash
kit check [--deep] [--fix] # проверить сервер; --deep – подключиться клиентом, --fix – исправить безопасное
kit update [--panel] # обновить сейчас; --panel – панель 3X-UI до проверенной версии
kit update --auto # ночное автообновление включить (--manual – выключить)
kit backup # копия сервера: подключения, ключи, пользователи
kit version # версии KIT, панели и ядра
``` ```
Копию из `kit backup` на новом чистом сервере поднимает `bash 3x-ui.sh --restore файл`. Если подписка выдавалась ### Флаги установки
на домене, а не на IP, клиентам достаточно сменить адрес в DNS.
</details>
<details> | Флаг | Что делает |
<summary><b>Если не подключается</b> – диагностика</summary>
```bash
kit check --deep # проверить сервер и подключиться к нему клиентом: видно, виноват сервер или сеть
kit net fp firefox # другой отпечаток клиента (вернуть: kit net fp chrome)
kit net site # другой сайт для маскировки
kit net on reality2 # дополнительный REALITY на другом порту
kit backup # не помогло – копия, затем перенос на другой адрес: на новом сервере bash 3x-ui.sh --restore файл
```
Один протокол не подключается – попробуйте соседний: у каждого свои сильные стороны (Hysteria2 работает по UDP,
остальные по TCP). В подписке Clash «Авто» выбирает сама, в остальных приложениях – вручную.
</details>
## VPN на роутере
Через прокси идут только нужные сервисы и выбранные устройства, остальное работает напрямую.
Порядок: **1.** установить программу на роутер → **2.** собрать конфиг в [генераторе](#генератор-конфигов-для-роутера) →
**3.** вставить команду из генератора.
> [!NOTE]
> На роутере работает [XKeen](https://github.com/jameszeroX/XKeen) автора jameszeroX, спасибо ему.
<details>
<summary><b>📶 Keenetic: XKeen (jameszeroX) · 8 шагов, 20–30 минут</b></summary>
Нужны роутер **Keenetic** или **Netcraze** с USB-портом, USB-флешка в формате **ext4** и свой сервер с VLESS
(например, из [этой инструкции](manuals/3x-ui.md)).
<details>
<summary><b>1. Компоненты KeeneticOS</b></summary>
В веб-интерфейсе: **Управление → Общие настройки → Изменить набор компонентов**. Отметьте компоненты
с галочками на картинке и установите. Роутер обновится и перезагрузится.
![Нужные компоненты KeeneticOS](manuals/assets/keenetic-components.svg)
</details>
<details>
<summary><b>2. Entware на флешку</b></summary>
1. Отформатируйте флешку в **ext4** и вставьте её в роутер.
2. Откройте её по сети (`\\192.168.x.x\`), создайте папку `install` и положите туда установщик под процессор роутера:
[mipsel](https://bin.entware.net/mipselsf-k3.4/installer/mipsel-installer.tar.gz),
[mips](https://bin.entware.net/mipssf-k3.4/installer/mips-installer.tar.gz) или
[aarch64](https://bin.entware.net/aarch64-k3.10/installer/aarch64-installer.tar.gz).
Не знаете, какой нужен? Модель роутера – в характеристиках на сайте Keenetic.
![Установщик Entware на флешке](manuals/assets/entware-installer.svg)
3. **Управление → OPKG**: выберите флешку и сохраните. Установка займёт несколько минут.
![Выбор накопителя для OPKG](manuals/assets/keenetic-opkg.svg)
</details>
<details>
<summary><b>3. Подключение по SSH</b></summary>
```bash
ssh root@192.168.x.x -p 222
```
Пароль по умолчанию `keenetic`. **Сразу смените его** командой `passwd`.
</details>
<details>
<summary><b>4. Шифрованный DNS</b></summary>
Пропишите шифрованные DNS-серверы (DoT или DoH) по
[инструкции Keenetic](https://support.keenetic.ru/ultra/kn-1811/ru/31543-dot-and-doh-proxy-servers-for-dns-requests-encryption.html):
без этого XKeen работает неправильно.
</details>
<details>
<summary><b>5. Токен для KeeneticOS 5.2 и новее</b></summary>
XKeen нужен токен доступа к роутеру. Создайте его в разделе **Пользователи и доступ**, вставьте в
[шаблон xkeen.json](https://github.com/jameszeroX/XKeen/releases/download/2.0.1_Beta/xkeen.json) и положите файл на роутер
по пути `/opt/etc/xkeen/xkeen.json`. Подробнее – в [вики XKeen](https://github.com/jameszeroX/XKeen/wiki/Порядок-установки).
</details>
<details>
<summary><b>6. Установка XKeen</b></summary>
Подключитесь по SSH и выполните:
```bash
opkg update && opkg upgrade && opkg install curl tar && cd /tmp
sh -c "$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/install.sh)"
```
Если GitHub недоступен, замените адрес на `https://cdn.jsdelivr.net/gh/jameszeroX/XKeen@main/install.sh`.
Установщик спросит ядро (Xray или Mihomo), геобазы и автозагрузку.
Затем соберите конфиг в [генераторе](#генератор-конфигов-для-роутера) (роутер Keenetic, нужное ядро), вставьте команду в SSH-консоль
и нажмите Enter: файлы запишутся, XKeen перезапустится.
![Конфигурационные файлы Xray](manuals/assets/xkeen-configs.svg)
</details>
<details>
<summary><b>7. Какие устройства через прокси</b></summary>
В веб-интерфейсе откройте **Приоритеты подключений → Политики доступа в Интернет**, создайте политику
с именем **`xkeen`** и перенесите в неё нужные устройства.
> [!WARNING]
> Без политики `xkeen` через прокси пойдёт трафик **всех** устройств в сети.
</details>
<details>
<summary><b>8. Проверка</b></summary>
- На роутере: `xkeen -status` – XKeen должен быть запущен.
- На устройстве из политики `xkeen` откройте сайт, показывающий IP-адрес: он должен совпасть с адресом вашего сервера,
а на остальных устройствах остаться прежним.
- Удобнее управлять настройками из браузера через [XKeen UI](https://github.com/zxc-rv/XKeen-UI).
</details>
Подробная версия с теми же шагами: [инструкция для Keenetic](manuals/xkeen-keenetic.md).
</details>
## Генератор конфигов для роутера
Вставьте ссылку на сервер или подписку, выберите роутер и ядро, отметьте нужные сервисы –
и получите готовый конфиг и одну команду, которая сама положит его на роутер.
Всё считается в браузере, ссылки никуда не отправляются.
Как поставить XKeen на роутер – в разделе [VPN на роутере](#vpn-на-роутере).
| Роутер | Ядро | Что получится | Генератор |
|---|---|---|---|
| Keenetic (XKeen) | Xray | `04_outbounds.json` и `05_routing.json`: серверы, выбор сервисов, реклама, свои сайты | [Открыть →](https://itsnotkubrick.github.io/3X-UI_KIT/tools/?core=xray) |
| Keenetic (XKeen) | Mihomo | `config.yaml` с автовыбором сервера, подпиской, Hysteria2, AmneziaWG и веб-панелью | [Открыть →](https://itsnotkubrick.github.io/3X-UI_KIT/tools/?core=mihomo) |
| OpenWrt (Nikki), бета | Mihomo | профиль для Nikki и команда, которая его включит | [Открыть →](https://itsnotkubrick.github.io/3X-UI_KIT/tools/?router=openwrt) |
## Полезное
- [XKeen](https://github.com/jameszeroX/XKeen) и его [вики](https://github.com/jameszeroX/XKeen/wiki) – документация по маршрутизации на Keenetic
- [XKeen UI](https://github.com/zxc-rv/XKeen-UI) – веб-интерфейс для XKeen
- [IP-адреса для AmneziaWG](https://github.com/RockBlack-VPN/ip-address) – актуальные списки от RockBlack
## Поддержать проект
Скрипты и инструкции бесплатные. Донат добровольный – он помогает оплачивать
тестовые серверы и держать скрипты в актуальном состоянии. Спасибо! 💜
| Способ | |
|---|---| |---|---|
| Российской картой, СБП, Tinkoff Pay | [Tribute](https://t.me/tribute/app?startapp=dRsY) | | `--protocols all\|minimal\|список` | какие протоколы ставить: `all`, `minimal` (только REALITY) или через запятую. Невыбранные не создаются вообще |
| Зарубежной картой, Apple Pay, Google Pay | [Buy Me a Coffee](https://buymeacoffee.com/relo.cate) | | `--domain ваш.домен` | свой домен для маскировки (A-запись на сервер, свободный порт 80 — сертификат Let's Encrypt выпустит установщик) |
| USDT (TRC-20) | `TS83ViXrdezUpp1eFadqj1rBhGLZaba1c1` | | `--sni сайт.ком` | маскировка под чужой сайт вместо своего домена |
| TON | `UQBchO4XFPwF9MMa_tjXpwqTo8IL2FhUDyllhYuFo8WM-Qbf` | | `--panel-on ip\|domain` | где открывать панель и подписку |
| Ethereum (ERC-20) | `0xC06F6B3A029d7Ea00705B7028490744e2BC16799` | | `--site N\|random\|off` | сайт-заглушка: конкретный из нашего набора, случайный (по умолчанию) или не трогать |
| `--ssh-port N` | перевести SSH на порт N (прежний закроется только после вашей проверки) |
| `--harden` / `--no-harden` | закрыть ping и включить автообновления безопасности / не трогать это |
| `--yes` | не задавать вопросы (для автоматизации) |
| `--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` | служебные |
## Благодарности ## Что происходит по шагам
3X-UI KIT построен на работе авторов этих проектов: 1. **Проверка сервера**: root, Debian/Ubuntu, память, место на диске, занятые порты 80/443.
[3X-UI](https://github.com/MHSanaei/3x-ui) · 2. **Проверка релиза**: файлы берутся из этого репозитория по тегу, проверяется **подпись `SHA256SUMS`**
[Xray-core](https://github.com/XTLS/Xray-core) · нашим ключом и SHA256 самого установщика. Не сошлось — ничего не запускается.
[Mihomo](https://github.com/MetaCubeX/mihomo) · 3. **Наши вопросы** (до установки): протоколы, порт SSH, пароль root, ping, автообновления.
[Hysteria](https://github.com/apernet/hysteria) · 4. **Установщик**: панель 3X-UI, только выбранные подключения, nginx на 443, сертификат, сайт-заглушка,
[AmneziaWG](https://github.com/amnezia-vpn) · служба подписки, правила ufw. Маскировку спрашивает он: свой домен, сосед по подсети или свой сайт.
[XKeen](https://github.com/jameszeroX/XKeen) Если выбран сосед — сначала идёт **скан подсети**, затем меню выбора сайта (см. выше).
5. **Наши действия** (без вопросов): правдоподобный ИТ-сайт-заглушка вместо авторской страницы,
`kit fix` (время и безопасные починки), закрыть ответ на ping, включить обновления безопасности,
перевести SSH на новый порт, сменить пароль root.
6. **Проверка**: `kit check` (порты, службы, подписка, сертификат, маскировка) и сводка
`kit local hardening`.
7. **В самом конце** — **доступ к панели 3X-UI** (адрес, логин, пароль из `/root/3x-ui.txt`) и
**подписка с QR-кодом** и ссылками по протоколам. После них скрипт уже ничего не делает и не спрашивает.
## Протоколы
Один порт 443 на всё: nginx разводит подключения по именам и путям, поэтому снаружи видно только
443/tcp (и Hysteria2 на 443/udp). Список и умолчание — как у проекта: невыбранные протоколы не
создаются вообще, ни подключения в панели, ни записей в nginx, ни ссылок в подписке.
| № | Протокол | Что это |
|---|---|---|
| 1 | **REALITY** | VLESS + Reality, основной вход. **По умолчанию** |
| 2 | REALITY-2 | второй REALITY на случайном высоком порту |
| 3 | Hysteria2 | быстрее на плохой сети (UDP 443) |
| 4 | XHTTP | VLESS + Reality под другим именем маскировки |
| 5 | VLESS-WS | VLESS через WebSocket |
| 6 | Trojan-gRPC | Trojan через gRPC |
| 7 | VMess-WS | VMess через WebSocket |
| 8 | Shadowsocks | Shadowsocks 2022 |
| 9 | TUIC | быстрый UDP-протокол |
| 10 | WireGuard | обычный WireGuard: легко распознаётся, поэтому по умолчанию не ставится |
| 11 | AmneziaWG | AmneziaWG: Mihomo, роутеры Keenetic |
| 12 | **AmneziaWG-3.1** | AmneziaWG с защитой заголовков: приложения AmneziaVPN. **По умолчанию** |
| 13 | MTProto | прокси для Telegram |
Наш набор по умолчанию — **REALITY + AmneziaWG-3.1**: работает почти везде и не требует
дополнительных объяснений на клиенте. Маскировку и сайт можно менять потом: `kit net site`,
`kit local site`.
**Сайт маскировки выбираете вы.** После скана подсети установщик спрашивает отдельно для REALITY
(найденный сосед или свой адрес) и отдельно для XHTTP/MTProto (найденный, свой или известный сайт —
список известных расширен Google, Amazon, Samsung, Yahoo, Microsoft, AMD, Intel, NVIDIA). Выбранный
адрес проверяется на TLS 1.3 + HTTP/2. Если XHTTP и MTProto не выбраны, второго вопроса нет.
Для протоколов, которые **не** выбраны, не делается ничего: ни подключений, ни записей в nginx, ни
ссылок в подписке, ни лишних обращений к сети (например, обход соседей запускается только когда он
действительно нужен).
## Сайт-заглушка
На 443 при неизвестном имени отдаётся страница нашего набора: небольшие правдоподобные ИТ-сайты
вместо стандартных «страница в разработке».
| № | Сайт | Тема |
|---|---|---|
| 1 | Radian Analytics | дата-аналитика: хранилище, отчёты, дашборды |
| 2 | Circuit Nine | DevOps-консалтинг: CI/CD, инфраструктура, наблюдаемость |
| 3 | Cortex Monitor | мониторинг серверов: проверки, алерты, сертификаты |
| 4 | Bluefin Software | разработка ПО на заказ |
| 5 | Vantage Security | аудит и пентест |
| 6 | Keystone IT | ИТ-поддержка офисов |
| 7 | Meridian Networks | колокейшн и транзит |
| 8 | Lumen Notes | заметки про Linux, nginx и сети |
| 9 | Evercore Backup | бэкапы и восстановление |
| 10 | Halyard Docs | документация проекта |
```bash
kit local site # случайный сайт из набора
kit local site --list # список с номерами
kit local site 7 # поставить конкретный
kit local site --show # какой стоит сейчас
```
Менять страницу нужно **этой командой**: `/var/www/kit/index.html` отдаёт nginx, а Hysteria2 держит
свою копию страницы внутри подключения. Команда обновляет обе — иначе 443/tcp и UDP начнут отвечать
разными страницами, и это заметно снаружи.
## Команды
На сервере после установки доступна команда `kit` (без аргументов — меню).
| Команда | Что делает |
|---|---|
| `kit user …` | `add`, `list`, `link`, `limit`, `on`, `off`, `del` — пользователи, ссылки, лимиты трафика и сроков |
| `kit net` | состояние подключений; `on`/`off` включить-выключить, `site` — сайт маскировки, `masq` — ответ Hysteria2 страницей, `dns` — DoH в подписках |
| `kit port` / `kit port set <имя> <порт>` | какие порты слушают подключения / сменить порт подключения |
| `kit reality add [порт]` | добавить второй REALITY |
| `kit check [--deep\|--fix]`, `kit fix` | проверка сервера (в том числе с самого сервера, как клиент) и безопасные починки |
| `kit update [--panel]`, `--auto`, `--manual` | обновление сборки и панели, режим автообновления |
| `kit backup` | копия сервера (панель, сертификаты, настройки) |
| `kit version` | версии сборки, панели и ядра |
Наш слой — `kit local` (справка: `kit local help`):
| Команда | Что делает |
|---|---|
| `kit local site` | сайт-заглушка: случайный, `--list`, номер, `--show` |
| `kit local ssh 2222` / `--status` / `--close-old 2222` | добавить порт SSH / какие слушают / закрыть прежние после проверки |
| `kit local ping off\|on\|status` | ответ на ICMP echo правилом ufw (видно в `ufw status`, отменяется одной командой) |
| `kit local passwd [юзер]`, `--generate` | смена пароля (спрашивает дважды, в логи не попадает) |
| `kit local upgrades on\|off\|status` | обновления безопасности |
| `kit local uninstall` | **снести установку целиком**: панель, службы и файлы kit, nginx и сайт, сертификаты, наши правки системы и правила ufw для 80/443. Спрашивает слово «удалить», есть `--dry-run`. **SSH не трогает** |
| `kit local hardening`, `masking`, `channel`, `seams`, `version` | сводка защиты, состояние маскировки, канал обновлений, какие места форк правит у автора |
У любой правки есть `--dry-run` — показывает план и ничего не меняет. Копии изменённых файлов
складываются в `/root/kit-local-backup-<дата>/`.
## Обновление и канал
- Сервер обновляется из **этого репозитория**: `kit update` и ночной таймер берут релиз с подписью
нашего ключа. Чужой релиз сервер не примет.
- Посмотреть, откуда берутся обновления и чем проверяется подпись: `kit local channel`.
- Управление автообновлением: `kit update --auto` (включить) и `kit update --manual` (выключить),
`kit update` — обновиться сейчас, `kit update --panel` — обновить саму панель 3X-UI.
- Проверить канал, ничего не устанавливая: установщик с `--verify-only` или `kit local channel`.
## Оговорки
- **Пароль root** никогда не передаётся флагом: значение в командной строке видно другим процессам.
Спрашивается в ходе установки или меняется `kit local passwd`.
- **Смена порта SSH** делается в два шага: новый порт добавляется, прежний продолжает работать, пока
вы не проверите вход во втором окне. Закрыть прежние: `kit local ssh --close-old 2222`.
- **`apt full-upgrade` не выполняется** — только обновления безопасности: панель и ядро Xray
закреплены на проверенных версиях.
- Версии панели и ядра в сборке сверяются с официальными хешами при выпуске релиза.
## Документация форка
| Файл | О чём |
|---|---|
| [FORK.md](FORK.md) | как устроен форк: где наши правки (швы), регламент обновления из авторского проекта, канал, подпись, грабли |
| [FORK-CHANGELOG.md](FORK-CHANGELOG.md) | что менялось в наших версиях |
| `kit local help` | команды форка прямо на сервере |
+9 -8
View File
@@ -1,8 +1,9 @@
# 3X-UI KIT 1.2.5 # 3X-UI KIT 1.2.1.9
c8d91ed459b4ba249166ab26361fa759ad6bdafe30158ee2cfaf3e0818e7644d scripts/3x-ui.sh # 3X-UI_autoVPS 1.2.1.9
e402fb8b1e98df934f86b40eaf700696d142a43d6e761e8beacd8fc3a4c9e396 scripts/hysteria2.sh 840b814246e0676a474511a26466b7b3406a8210c2b1e7097579b578d7c71257 scripts/3x-ui.sh
235e314bfc6603622e259e45819ecc03422dcb5fd18666f4d839b24f626d2a26 scripts/kit.sh a85f01f285156131e0a51a9de5fb2bc8ea76d5b49763bf0d544ca294b69173ed scripts/hysteria2.sh
eac3e47491ce86487f05160952ff5b4ad8a3821954f082eee3516e40575fb574 scripts/kit-sub.py 89b6c8635d77558dd361cf64c5a01cde08fbe2964ad6f63ba22c17599f1e620c scripts/kit.sh
30579dfae24239d1c797dc8efac5b2b49116bc05058923df184b382f33fe265b scripts/kit-local.sh f214671464e1c2e174e5af9df42c95567a9b4a9141f31a39f7df2e971e89d240 scripts/kit-sub.py
40c8612011a72f4b5f1f754b593c51b5d2bff547a0e9fca2602f3d3fbd1923f9 scripts/3x-ui-local.sh 5d07c87452057e1c73e0f3af008074f94f5be6175817b2d6f94a93f69b0533b7 scripts/kit-local.sh
fe2b7c237b5a6e42818aa3e4f7fc5820f49c32289c32c96260ef89a8c8f6d3d7 VERSION bfaac8949c1e9f92fe92f1f0f6ff59ac7f37fad74cbf62439f327bc3144a8a06 scripts/3x-ui-local.sh
bb43cbd01c2071d4aacdce23a00da7d86a126799e511a521c8a3c6818001a670 VERSION
+2 -2
View File
@@ -1,6 +1,6 @@
-----BEGIN SSH SIGNATURE----- -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgozRJ+f9RB6qO9S9j3BWCDtC0bV U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgozRJ+f9RB6qO9S9j3BWCDtC0bV
FHakRSSqtQPljLoQUAAAARM3gtdWkta2l0LXJlbGVhc2UAAAAAAAAABnNoYTUxMgAAAFMA FHakRSSqtQPljLoQUAAAARM3gtdWkta2l0LXJlbGVhc2UAAAAAAAAABnNoYTUxMgAAAFMA
AAALc3NoLWVkMjU1MTkAAABAh7WjPC1qvJKsop5cPtRaLQQCNUe/jmQp4Ox/1dqqlsT6Ke AAALc3NoLWVkMjU1MTkAAABA+3t9MmBZ+iwMvnKIfkihiqb0dX/qo8Ca5pTjBFec4v5ulG
NmQAWSSoHRyhUTKJaMgnYwojd7w/zFeSg4USqLDQ== L4RtQ+cG1mBiv66u5h6Lh+yI/JgNzd/0Yccr5NBw==
-----END SSH SIGNATURE----- -----END SSH SIGNATURE-----
+1 -1
View File
@@ -1 +1 @@
1.2.5 1.2.1.9
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="860" height="190" viewBox="0 0 860 190" role="img" aria-label="3X-UI autoVPS — сборка форка">
<style>.t{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}
.m{font-family:'JetBrains Mono',ui-monospace,Menlo,Consolas,monospace}</style>
<rect x="0.5" y="0.5" width="859" height="189" rx="16" fill="#15110f" stroke="#43372e"/>
<circle cx="748" cy="46" r="58" fill="#93e06f" opacity="0.07"/>
<circle cx="808" cy="140" r="34" fill="#93e06f" opacity="0.05"/>
<text x="44" y="86" class="t" font-size="46" font-weight="700" fill="#fbf6ec">3X-UI <tspan fill="#93e06f">autoVPS</tspan></text>
<text x="44" y="118" class="t" font-size="16" fill="#cbbfa9">сервер одной командой: панель 3X-UI, VPN-протоколы, один порт 443, одна подписка</text>
<text x="44" y="152" class="m" font-size="13" fill="#8a8073">свой канал обновлений · подписанные релизы · 13 протоколов · 10 сайтов-заглушек</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

+474 -71
View File
@@ -1,39 +1,54 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 3X-UI KIT (форк hacker07) — установка сервера одной командой из нашего канала. # 3X-UI_autoVPS (форк hacker07) — установка сервера одной командой из нашего канала.
# #
# Что делает (по шагам): # Что делает (по шагам):
# 1) проверяет, что мы на чистом Debian/Ubuntu и под root, и что есть curl, ssh-keygen, openssl; # 1) проверяет сервер и наличие curl, ssh-keygen, openssl;
# 2) берёт из нашего Gitea релиз своей же версии, проверяет ПОДПИСЬ SHA256SUMS и SHA256 установщика; # 2) берёт из нашего Gitea релиз своей же версии, проверяет ПОДПИСЬ SHA256SUMS и SHA256 установщика;
# 3) запускает установщик автора (scripts/3x-ui.sh) — панель 3X-UI, протоколы, сайт-заглушка, # 3) запускает установщик автора (scripts/3x-ui.sh) — панель 3X-UI, протоколы, сайт-заглушка,
# маскировка, сертификат, подписка, ufw; все обычные флаги установщика можно передавать как есть; # маскировка, сертификат, подписка, ufw. Флаги установщика можно передавать как есть,
# 4) досыпает то, чего у автора нет (поведение из StressOzz/vps-setup, но безопаснее): # а можно ничего не передавать — тогда он задаст вопросы сам (в том числе про домен);
# ответ на ping закрывается правилом ufw, включаются обновления безопасности; # 4) спрашивает, какие протоколы ставить: невыбранные не создаются вообще — ни подключения в панели,
# 5) при желании добавляет второй порт SSH (--ssh-port 2222) — прежний остаётся рабочим; # ни записей в nginx, ни ссылок в подписке (за это отвечает флаг автора --protocols);
# 6) в конце: kit fix (время и безопасные починки), kit check, kit local hardening. # 5) ставит сайт-заглушку: случайный правдоподобный ИТ-сайт из нашего набора (10 штук;
# --site N — конкретный, --no-site — не трогать авторскую страницу);
# 6) спрашивает и делает то, чего у автора нет: закрыть ответ на ping, включить обновления
# безопасности, перевести SSH на другой порт, сменить пароль root (поведение из StressOzz/vps-setup,
# но безопаснее);
# 7) в конце: kit fix (время и безопасные починки), kit check, kit local hardening;
# 8) последним — подписка, QR-код и ссылки по протоколам: то, что нужно сохранить.
# #
# Запуск (примеры): # Запуск:
# bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v1.2.3/scripts/3x-ui-local.sh) --domain vpn.example.com # # одна команда, дальше — вопросы:
# ... 3x-ui-local.sh --sni dl.google.com --harden # bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui-local.sh)
# ... 3x-ui-local.sh --ssh-port 2222 --no-harden # # или сразу с ответами, без вопросов:
# ... 3x-ui-local.sh --domain vpn.example.com
# ... 3x-ui-local.sh --sni dl.google.com --no-harden --ssh-port 2222
# # без вопросов, всё по умолчанию (для автоматизации):
# ... 3x-ui-local.sh --yes
# #
# Флаги самого обёрточного скрипта (остальные уходят установщику автора): # Флаги этого скрипта (остальные уходят установщику автора):
# --no-harden не отключать ping и не включать автообновления безопасности # --domain ДОМЕН, --sni САЙТ, --panel-on ip|domain, --host IP, --user ИМЯ, --port N, --restore ФАЙЛ
# --ssh-port N добавить порт SSH N (прежний порт продолжает работать) # --harden закрыть ping и включить автообновления, без вопросов
# --no-time не запускать kit fix (синхронизация времени и безопасные починки) # --no-harden не трогать ping и автообновления
# --no-check не запускать в конце kit check # --ssh-port N добавить порт SSH N (прежний порт продолжает работать)
# --dry-run показать план и ничего не делать # --yes ни о чём не спрашивать (ping закрою, автообновления включу, порт и пароль — нет)
# -h | --help эта справка # --no-check не запускать в конце kit check
# --clean убрать прошлую установку и поставить заново (SSH не трогает)
# --auto-sni не предлагать выбор сайта маскировки (иначе покажем меню по скану подсети)
# --verify-only только проверить релиз (подпись и SHA256) и выйти
# --dry-run показать план и ничего не делать
# -h | --help эта справка
# #
# Наш файл: авторские релизы его не перезаписывают, а правки форка живут в scripts/kit-local.sh. # Наш файл: авторские релизы его не перезаписывают, а правки форка живут в scripts/kit-local.sh.
set -Eeuo pipefail set -Eeuo pipefail
KIT_VERSION="1.2.5" # версия сборки форка; должна совпадать с VERSION релиза KIT_VERSION="1.2.1.9" # версия сборки форка; должна совпадать с VERSION релиза
GITEA="${KIT_FORK_BASE:-https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS}" GITEA="${KIT_FORK_BASE:-https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS}"
NS="3x-ui-kit-release" NS="3x-ui-kit-release"
SIG_ID="releases@3x-ui-kit" SIG_ID="releases@3x-ui-kit"
# Публичный ключ форка. Меняется только вместе с ключом подписи (тогда обновляется и здесь, и в kit). # Публичный ключ форка. Меняется только вместе с ключом подписи (тогда обновляется и здесь, и в kit).
SIGNER_PUB='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI KIT fork releases (hacker07)' SIGNER_PUB='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)'
G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m' G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m'
[[ -t 1 ]] || { G=; Y=; R=; B=; D=; N=; } [[ -t 1 ]] || { G=; Y=; R=; B=; D=; N=; }
@@ -43,40 +58,58 @@ die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; }
usage() { usage() {
cat <<'EOF' cat <<'EOF'
3X-UI KIT (форк hacker07) — установка сервера одной командой из нашего канала. 3X-UI_autoVPS (форк hacker07) — установка сервера одной командой из нашего канала.
bash <(curl -fsSL <наш Gitea>/raw/tag/v<версия>/scripts/3x-ui-local.sh) [флаги…] bash <(curl -fsSL <наш Gitea>/raw/branch/main/scripts/3x-ui-local.sh) [флаги…]
Всё, что скрипт не знает сам, он передаёт установщику автора как есть: Без флагов скрипт спрашивает: какие протоколы ставить (невыбранные не создаются вообще),
маскировку и домен — это вопросы установщика автора; затем про ответ на ping, автообновления,
второй порт SSH и пароль root — это наши вопросы.
Флаги, которые скрипт передаёт установщику автора как есть:
--domain vpn.example.com свой домен для маскировки (A-запись на этот сервер, свободный 80) --domain vpn.example.com свой домен для маскировки (A-запись на этот сервер, свободный 80)
--sni сайт.com маскировка под чужой сайт --sni сайт.com маскировка под чужой сайт
--panel-on domain | ip где открывать панель и подписку --panel-on domain | ip где открывать панель и подписку
--host IP, --user имя, --port N, --protocols …, --restore файл --host IP, --user имя, --port N, --protocols …, --restore файл
Флаги этого скрипта: Флаги этого скрипта:
--no-harden не отключать ответ на ping и не включать автообновления безопасности --harden закрыть ping и включить автообновления, без вопросов
--ssh-port N добавить порт SSH N (прежний порт продолжает работать) --no-harden не трогать ping и автообновления
--no-time не запускать kit fix (время и безопасные починки) --ssh-port N перевести SSH на порт N (прежний закроется после проверки входа)
--yes ни о чём не спрашивать (для автоматизации)
--site N|random|off сайт-заглушка: конкретный из нашего набора, случайный (по умолчанию) или
не трогать авторскую страницу (то же: --no-site)
--no-sub не показывать в конце подписку, доступ к панели и QR-код
--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 показать план и ничего не делать
-h | --help эта справка -h | --help эта справка
Что скрипт делает: проверяет сервер → скачивает релиз своей же версии из нашего Gitea,
проверяет подпись SHA256SUMS и SHA256 установщика → запускает установщик автора → досыпает
защиту (ping off правилом ufw, автообновления безопасности, при желании второй порт SSH) →
kit fix и kit check.
EOF EOF
} }
# ------------------------------------------------------------------ разбор флагов # ------------------------------------------------------------------ разбор флагов
INSTALLER_ARGS=(); harden=yes; ssh_port=""; fix_time=yes; do_check=yes; dry=no; verify_only=no INSTALLER_ARGS=(); harden=auto; ssh_port=""; fix_time=yes; do_check=yes; dry=no; verify_only=no; assume_yes=no
site=random # заглушка: случайный сайт из нашего набора (off — не трогать)
show_sub=yes # в самом конце показать подписку, QR-код и ссылки
clean=no # --clean: убрать прошлую установку перед новой
pick_sni=yes # показывать меню выбора сайта маскировки по результатам скана
while (($#)); do while (($#)); do
case $1 in case $1 in
--harden) harden=yes ;;
--no-harden) harden=no ;; --no-harden) harden=no ;;
--ssh-port) ssh_port=${2:-}; shift || die "--ssh-port: нужен номер порта" ;; --ssh-port) ssh_port=${2:-}; shift || die "--ssh-port: нужен номер порта" ;;
--ssh-port=*) ssh_port=${1#*=} ;; --ssh-port=*) ssh_port=${1#*=} ;;
--yes | -y) assume_yes=yes ;;
--site) site=${2:-random}; shift || die "--site: нужен номер сайта (1..10), random или off" ;;
--site=*) site=${1#*=} ;;
--no-site) site=off ;;
--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 ;;
@@ -86,20 +119,51 @@ while (($#)); do
esac esac
shift shift
done done
[[ -z $ssh_port || $ssh_port =~ ^[0-9]{1,5}$ ]] || die "--ssh-port: нужен номер порта, например 2222" [[ -z $ssh_port ]] || { [[ $ssh_port =~ ^[0-9]{1,5}$ ]] && ((10#$ssh_port >= 1024 && 10#$ssh_port <= 65535)); } \
|| die "--ssh-port: нужно число 1024–65535, например 2222"
if [[ $site != off && $site != random ]]; then
# 10 — число сайтов в наборе (scripts/kit-local.sh, KL_SITE_TITLES); добавите сайт — поднимите границу
[[ $site =~ ^[0-9]{1,2}$ ]] && ((10#$site >= 1 && 10#$site <= 10)) \
|| die "--site: random, off или номер 1..10 (список: kit local site --list)"
fi
# Если --protocols задан флагом — не спрашиваем и не дублируем его.
proto_flag=""
for ((_i = 0; _i < ${#INSTALLER_ARGS[@]}; _i++)); do
[[ ${INSTALLER_ARGS[_i]} == --protocols ]] && proto_flag=${INSTALLER_ARGS[_i + 1]:-all}
done
pre() { printf ' %s\n' "$*"; } pre() { printf ' %s\n' "$*"; }
ASK_SRC=${KIT_ASK_SRC:-/dev/tty} # откуда читать ответы (в тестах подменяется файлом)
interactive() { [[ $assume_yes == no && -r $ASK_SRC ]]; }
if [[ $dry == yes ]]; then 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), подпись проверяется ключом форка"
if ((${#INSTALLER_ARGS[@]})); then pre "установщик автора: bash scripts/3x-ui.sh ${INSTALLER_ARGS[*]}"; else pre "установщик автора: bash scripts/3x-ui.sh (без доп. флагов)"; fi [[ $clean == yes ]] && pre "прошлая установка: уберу её перед установкой (--clean), SSH не трону"
pre "маскировка: своя заглушка/домен — как решит установщик автора" if [[ $pick_sni == yes ]]; then pre "маскировка: просканирую подсеть и предложу выбрать сайт (--auto-sni — автоподбор)"
if [[ $harden == yes ]]; then pre "защита: ping off (правилом ufw) + автообновления безопасности"; else pre "защита: пропускаю (--no-harden)"; fi else pre "маскировка: подберу сайт сам, без вопросов (--auto-sni)"; fi
[[ -n $ssh_port ]] && pre "порт SSH: добавляю $ssh_port (прежний остаётся рабочим)" if ((${#INSTALLER_ARGS[@]})); then pre "установщик автора: bash scripts/3x-ui.sh ${INSTALLER_ARGS[*]}"; else pre "установщик автора: bash scripts/3x-ui.sh — спросит про маскировку и домен"; fi
[[ $fix_time == yes ]] && pre "после: kit fix (время, безопасные починки)" if [[ $harden == auto ]]; then
if interactive; then pre "защита: спрошу про ответ на ping и автообновления"
else pre "защита: без терминала — закрыть ping и включить автообновления"; fi
elif [[ $harden == yes ]]; then pre "защита: закрыть ping и включить автообновления (--harden)"
else pre "защита: пропускаю (--no-harden)"; fi
if [[ -n $proto_flag ]]; then pre "протоколы: ${proto_flag} (задано флагом)"
elif interactive; then pre "протоколы: спрошу (Enter — REALITY + AmneziaWG-3.1, «все» — набор автора)"
else pre "протоколы: REALITY + AmneziaWG-3.1 (без терминала вопроса нет)"; fi
if [[ -n $ssh_port ]]; then pre "порт SSH: перевожу на $ssh_port (прежний — после проверки входа)"
elif interactive; then pre "порт SSH: спрошу (Enter — оставить текущий)"; fi
if interactive; then pre "пароль root: спрошу (по умолчанию — не менять)"; fi
case $site in
off) pre "заглушка: оставляю авторскую страницу (--no-site)" ;;
random) pre "заглушка: случайный ИТ-сайт из нашего набора (10 штук)" ;;
*) pre "заглушка: сайт №$site из нашего набора (список: kit local site --list)" ;;
esac
[[ $fix_time == yes ]] && pre "после: kit fix (время, службы, права) — тихо, без описания команды"
[[ $do_check == yes ]] && pre "проверка: kit check + kit local hardening" [[ $do_check == yes ]] && pre "проверка: kit check + kit local hardening"
[[ $show_sub == yes ]] && pre "в самом конце: доступ к панели 3X-UI, подписка, QR-код и ссылки (--no-sub — не надо)"
echo echo
say "запуск без --dry-run выполнит всё это на сервере" say "запуск без --dry-run выполнит всё это на сервере"
exit 0 exit 0
@@ -114,7 +178,7 @@ say "беру релиз v$KIT_VERSION и проверяю подпись"
tmp=$(mktemp -d) tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT trap 'rm -rf "$tmp"' EXIT
raw="$GITEA/raw/tag/v$KIT_VERSION" raw="$GITEA/raw/tag/v$KIT_VERSION"
for f in SHA256SUMS SHA256SUMS.sig scripts/3x-ui.sh; do for f in SHA256SUMS SHA256SUMS.sig; do
mkdir -p "$tmp/$(dirname "$f")" mkdir -p "$tmp/$(dirname "$f")"
curl -fsSL --retry 3 -m 60 -o "$tmp/$f" "$raw/$f" || die "не скачался $f из $raw (Gitea доступна с сервера?)" curl -fsSL --retry 3 -m 60 -o "$tmp/$f" "$raw/$f" || die "не скачался $f из $raw (Gitea доступна с сервера?)"
done done
@@ -123,9 +187,21 @@ ssh-keygen -Y verify -f "$tmp/allowed_signers" -I "$SIG_ID" -n "$NS" -s "$tmp/SH
|| die "подпись релиза v$KIT_VERSION не сошлась с ключом форка — это не наш релиз, ничего не запускаю" || die "подпись релиза v$KIT_VERSION не сошлась с ключом форка — это не наш релиз, ничего не запускаю"
sum=$(awk '{f = $2; sub(/^\*/, "", f)} f == "scripts/3x-ui.sh" {print $1}' "$tmp/SHA256SUMS") sum=$(awk '{f = $2; sub(/^\*/, "", f)} f == "scripts/3x-ui.sh" {print $1}' "$tmp/SHA256SUMS")
[[ $(printf '%s' "$sum" | wc -c) -eq 64 ]] || die "в SHA256SUMS нет установщика — релиз собран неверно" [[ $(printf '%s' "$sum" | wc -c) -eq 64 ]] || die "в SHA256SUMS нет установщика — релиз собран неверно"
# Качаем ВСЕ файлы релиза и сверяем каждый с подписанным списком. Так у нас есть и scripts/3x-ui.sh
# (установщик), и scripts/kit-local.sh (наша страница-заглушка), и всё остальное. Раньше брали только
# установщик: заглушка не готовилась («kl_site_html: command not found»), а проверка целостности была неполной.
while read -r sum2 name2; do
case $sum2 in \#* | "") continue ;; esac
case $name2 in SHA256SUMS | SHA256SUMS.sig) continue ;; esac
mkdir -p "$tmp/$(dirname "$name2")"
curl -fsSL --retry 3 -m 60 -o "$tmp/$name2" "$raw/$name2" || die "не скачался $name2 из $raw (Gitea доступна с сервера?)"
got2=$(sha256sum "$tmp/$name2" | awk '{print $1}')
[[ $got2 == "$sum2" ]] || die "SHA256 не совпал у $name2 — не запускаю"
done <"$tmp/SHA256SUMS"
got=$(sha256sum "$tmp/scripts/3x-ui.sh" | awk '{print $1}') got=$(sha256sum "$tmp/scripts/3x-ui.sh" | awk '{print $1}')
[[ $got == "$sum" ]] || die "SHA256 установщика не совпал с подписанным — не запускаю" [[ $got == "$sum" ]] || die "SHA256 установщика не совпал с подписанным — не запускаю"
bash -n "$tmp/scripts/3x-ui.sh" || die "установщик не проходит проверку синтаксиса" bash -n "$tmp/scripts/3x-ui.sh" || die "установщик не проходит проверку синтаксиса"
[[ -r $tmp/scripts/kit-local.sh ]] || die "в релизе нет scripts/kit-local.sh — сборка неполная"
printf ' ✓ подпись сошлась, SHA256 установщика совпал\n' printf ' ✓ подпись сошлась, SHA256 установщика совпал\n'
if [[ $verify_only == yes ]]; then if [[ $verify_only == yes ]]; then
@@ -145,56 +221,383 @@ printf ' %s\n' "${PRETTY_NAME:-Linux}, ядро $(uname -r), памяти $(awk
df -P / | awk 'NR==2 {gsub("%","",$5); if ($5 >= 95) exit 1}' || warn "диск занят почти полностью — установка может не пройти" df -P / | awk 'NR==2 {gsub("%","",$5); if ($5 >= 95) exit 1}' || warn "диск занят почти полностью — установка может не пройти"
for p in 443 80; do ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$p\$" && warn "порт $p уже кем-то занят"; done for p in 443 80; do ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$p\$" && warn "порт $p уже кем-то занят"; done
# ------------------------------------------------------------------ какие протоколы ставить
# Список, порядок и набор по умолчанию — как у установщика автора (scripts/3x-ui.sh:
# ALL_PROTOS / DEFAULT_PROTOS). Невыбранные протоколы не создаются вообще: под них не будет
# ни подключения в панели, ни записей в nginx, ни ссылок в подписке.
ALL_PROTOS=(reality reality2 hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtproto)
PROTO_HINT=(
"REALITY основной: VLESS + Reality, порт 443"
"REALITY-2 второй REALITY на случайном высоком порту"
"Hysteria2 быстрее на плохой сети (UDP 443)"
"XHTTP VLESS + Reality под другим именем маскировки"
"VLESS-WS VLESS через WebSocket"
"Trojan-gRPC Trojan через gRPC"
"VMess-WS VMess через WebSocket"
"Shadowsocks Shadowsocks 2022"
"TUIC быстрый UDP-протокол"
"WireGuard обычный WireGuard: легко распознаётся, обычно не ставят"
"AmneziaWG AmneziaWG: Mihomo, роутеры Keenetic"
"AmneziaWG-3.1 AmneziaWG с защитой заголовков: AmneziaVPN"
"MTProto прокси для Telegram"
)
# Наш набор по умолчанию: VLESS REALITY и AmneziaWG-3.1 — самый совместимый минимум.
# Набор автора (флаг --protocols all) доступен ответом «все».
AUTHOR_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic awg awg3 mtproto)
DEFAULT_PROTOS=(reality awg3)
proto_menu() {
local i n mark
say "какие протоколы поставить?" >&2
for i in "${!ALL_PROTOS[@]}"; do
n=$((i + 1)) mark=" "
[[ " ${DEFAULT_PROTOS[*]} " == *" ${ALL_PROTOS[i]} "* ]] && mark="*"
printf ' %s %2d. %s\n' "$mark" "$n" "${PROTO_HINT[i]}" >&2
done
printf ' %s\n' "${D}[* — по умолчанию: REALITY + AmneziaWG-3.1. «все» — набор автора (всё, кроме WireGuard).${N}" >&2
printf ' %s\n' "${D}[Невыбранные протоколы не создаются вообще: ни подключения, ни записей в nginx, ни ссылок в подписке.]${N}" >&2
}
proto_parse() { # ответ → выбранные имена через запятую; код 1, если не разобрал
local ans=${1,,} tok i all=" ${ALL_PROTOS[*]} " picked=""
ans=${ans//[[:space:]]/,} # «1 3 11» и «1,3,11» — одно и то же
[[ -n $ans ]] || { printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0; }
case $ans in
all | все | всё) printf '%s' "$(IFS=,; echo "${AUTHOR_PROTOS[*]}")"; return 0 ;;
minimal | минимум) printf '%s' "reality"; return 0 ;;
esac
local IFS=','
for tok in $ans; do
[[ -n $tok ]] || continue # двойные запятые и пробелы — не ошибка
if [[ $tok =~ ^[0-9]{1,2}$ ]]; then
i=$((10#$tok - 1))
((i >= 0 && i < ${#ALL_PROTOS[@]})) || return 1
tok=${ALL_PROTOS[i]}
fi
[[ $all == *" $tok "* ]] || return 1 # только имена из списка автора
[[ ,$picked, == *",$tok,"* ]] || picked+="${picked:+,}$tok"
done
[[ -n $picked ]] || return 1
printf '%s' "$picked"
}
ask_protocols() { # печатает итоговый список через запятую
local i ans picked
interactive || { printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0; }
proto_menu
for i in 1 2 3; do
printf '\n%sНомера через пробел (например 1 12) или «все»; Enter — REALITY + AmneziaWG-3.1: %s' "$B" "$N" >&2
ans=""
read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true
if picked=$(proto_parse "$ans"); then printf '%s' "$picked"; return 0; fi
warn "не понял «$ans» — нужны номера из списка через пробел или «все»"
done
warn "оставляю набор по умолчанию"
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
protos=$(ask_protocols)
INSTALLER_ARGS+=(--protocols "$protos")
else
protos=$proto_flag
fi
say "протоколы: $protos — под остальные не создаётся ничего"
# ------------------------------------------------------------------ вопросы (всё спросим ДО установки)
# После установщика на экране появляются доступы к панели: там вопросы уже неуместны. Поэтому
# спрашиваем всё заранее, а действия выполняем после установки (см. «наши дополнения» ниже).
ask_yes() { # "вопрос" y|n — по умолчанию
local q=$1 def=$2 ans=""
interactive || { [[ $def == y ]]; return; }
printf '\n%s [%s]: ' "$q" "$([[ $def == y ]] && echo 'Y/n' || echo 'y/N'])"
read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true
ans=${ans,,}; ans=${ans//[[:space:]]/}
[[ -z $ans && $def == y ]] && return 0
[[ $ans =~ ^(y|yes|д|да)$ ]]
}
# порт SSH: по флагу или по вопросу. Смена делается в два шага: сначала ДОБАВИТЬ новый порт
# (прежний продолжает работать), потом закрыть прежние — иначе одна опечатка оставляет сервер без доступа.
ssh_ports_now() {
local p
p=$(kit local ssh --status 2>/dev/null | sed -n 's/^порты SSH сейчас: //p')
[[ -n $p ]] || p=$(sshd -T 2>/dev/null | awk '$1 == "port" {print $2}' | paste -sd, -)
[[ -n $p ]] || p=$(grep -rhiE '^[[:space:]]*Port[[:space:]]+[0-9]+' /etc/ssh/sshd_config /etc/ssh/sshd_config.d 2>/dev/null | awk '{print $2}' | paste -sd, -)
printf '%s' "${p:-22}"
}
ask_ssh_port() { # пусто — оставить как есть; иначе номер нового порта
local cur ans try
cur=$(ssh_ports_now)
interactive || return 0
for try in 1 2 3; do
printf '\n%sСменить порт SSH? Сейчас %s. Введите новый номер, Enter — оставить как есть: %s' "$B" "$cur" "$N" >&2
ans=""
read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true
ans=${ans//[[:space:]]/}
[[ -n $ans ]] || { say "порт SSH оставляю как есть ($cur)" >&2; return 0; }
if ! [[ $ans =~ ^[0-9]{1,5}$ ]] || ((10#$ans < 1024 || 10#$ans > 65535)); then
warn "«$ans» не похоже на порт (нужно число 1024–65535)"
continue
fi
if [[ ,$cur, == *",$ans,"* ]]; then
say "порт $ans уже слушает SSH — менять нечего" >&2; return 0
fi
if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$ans\$"; then
warn "порт $ans/tcp занят другой программой"
continue
fi
printf '%s' "$ans"; return 0
done
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)
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, протоколы, сайт-заглушка, подписка)" say "запускаю установщик автора (панель 3X-UI, протоколы, сайт-заглушка, подписка)"
if ((${#INSTALLER_ARGS[@]})); then printf ' флаги: %s\n' "${INSTALLER_ARGS[*]}"; fi if ((${#INSTALLER_ARGS[@]})); then printf ' флаги: %s\n' "${INSTALLER_ARGS[*]}"; else printf ' флагов нет: установщик задаст вопросы (маскировка, домен)\n'; fi
# stdin: если есть терминал — отдаём его установщику (там есть вопросы), иначе /dev/null, # stdin: если есть терминал — отдаём его установщику (там есть вопросы), иначе /dev/null,
# иначе установщик начнёт читать наш собственный скрипт вместо ответов. # иначе установщик начнёт читать наш собственный скрипт вместо ответов.
if [[ -t 0 ]] || { : </dev/tty; } 2>/dev/null; then if [[ -r /dev/tty ]]; then
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/tty || die "установщик остановился с ошибкой — он ничего не тронул дальше своего лога" bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/tty || installer_failed
else else
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/null || die "установщик остановился с ошибкой (нет терминала — вопросы задать некому)" bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/null || installer_failed
fi fi
command -v kit >/dev/null || die "после установки нет команды kit — что-то пошло не так, смотрите вывод выше" 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 if [[ $fix_time == yes ]]; then
say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)" say "kit fix: безопасные починки (время, службы, права)"
kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод" kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод"
fi fi
if [[ $harden == yes ]]; then # Действия по ответам, которые были заданы ДО установщика (см. блок вопросов выше).
say "защита сервера: закрываю ответ на ping и включаю обновления безопасности" if [[ $want_ping == yes ]]; then
kit local ping off || warn "не удалось закрыть ping — повторите: kit local ping off" say "закрываю ответ на ping (правилом ufw)"
kit local upgrades on || warn "не удалось включить автообновления — повторите: kit local upgrades on" 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 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
# ------------------------------------------------------------------ итог: коротко и по делу
if [[ $do_check == yes || $show_sub == yes ]]; then
# 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
protos_show=$(tr '\n' ' ' </etc/kit/kl-protos 2>/dev/null || true)
cat <<EOF cat <<EOF
${Y}Дальше обязательно:${N} ${B}────────────── Установка завершена ──────────────${N}
проверьте вход во ВТОРОМ окне: ssh -p $ssh_port root@<IP сервера> сборка: 3X-UI_autoVPS $KIT_VERSION (канал: наш Gitea)
убедились — закройте прежние порты: kit local ssh --close-old $ssh_port сервер: ${ip_show:-?}${os_show:+ · $os_show}
протоколы: ${protos_show:-—}
сертификат: $cert_show
проверка: $([[ $problems == 0 ]] && echo 'замечаний нет' || echo "замечаний: $problems (подробности: kit check)")
${B}───────────────────────────────────────────────────${N}
${B}Доступ к панели${N} (файл /root/3x-ui.txt, виден только root)
EOF
# Автор пишет эти строки без ведущих пробелов (RESULT=/root/3x-ui.txt), поэтому ищем их как есть.
if grep -aqE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt 2>/dev/null; then
grep -aE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt | sed 's/^[[:space:]]*/ /'
printf ' файл целиком (там же подписка и ссылки по протоколам): /root/3x-ui.txt\n'
else
warn "данных панели в /root/3x-ui.txt нет — смотрите вывод установщика выше"
fi
cat <<EOF
${B}Ссылки для подключения и QR-код${N} — в меню kit
kit → Пользователи → Показать ссылки из консоли: kit local links
EOF
if [[ -n $ports_open ]]; then
cat <<EOF
${B}Порты${N}
открыто: $ports_open
что нужно и что лишнее: kit local ports закрыть лишние: kit local ports --close-extra
EOF
fi
if [[ -n ${ssh_port:-} ]]; then
cat <<EOF
${Y}SSH переведён на порт $ssh_port.${N} Прежние (${ssh_ports_before:-22}) пока открыты намеренно:
1) проверьте вход во втором окне: ssh -p $ssh_port root@${ip_show:-<IP>}
2) закройте прежние порты: kit local ssh --close-old $ssh_port
EOF
fi
cat <<EOF
если что-то не так: kit check --deep, затем kit fix
EOF EOF
fi fi
# ------------------------------------------------------------------ проверка
if [[ $do_check == yes ]]; then
say "проверяю, что получилось"
kit check || warn "kit check нашёл замечания — читайте выше; безопасные починки: kit fix"
kit local hardening
fi
cat <<EOF
${G}${B}Готово.${N} Сервер поставлен из нашей сборки (3X-UI KIT $KIT_VERSION, канал: наш Gitea).
подписка и ссылки: kit user link admin (или страница подписки из вывода установщика)
если что-то не так: kit check --deep, затем kit fix
вернуться к авторскому каналу: положить авторский kit поверх и выполнить kit update --force
EOF
+546 -145
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -2,18 +2,18 @@
# Hysteria2 – https://github.com/itsnotkubrick/3X-UI_KIT # Hysteria2 – https://github.com/itsnotkubrick/3X-UI_KIT
# #
# Отдельный установщик Hysteria2 больше не поддерживается: Hysteria2 входит в основную # Отдельный установщик Hysteria2 больше не поддерживается: Hysteria2 входит в основную
# установку 3X-UI KIT (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update` # установку 3X-UI_autoVPS (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`
# получила из подписанного релиза понятное сообщение. Ничего не ставит, не удаляет и не # получила из подписанного релиза понятное сообщение. Ничего не ставит, не удаляет и не
# останавливает: сервер Hysteria2, установленный раньше, продолжает работать как был. # останавливает: сервер Hysteria2, установленный раньше, продолжает работать как был.
# Код выхода 1 при любом запуске (и при `__update_binary`) – старый `hy2 update` на нём # Код выхода 1 при любом запуске (и при `__update_binary`) – старый `hy2 update` на нём
# останавливается и не заменяет команду hy2. # останавливается и не заменяет команду hy2.
KIT_VERSION="1.2.5" KIT_VERSION="1.2.1.9"
cat >&2 <<'MSG' cat >&2 <<'MSG'
Отдельный установщик Hysteria2 больше не поддерживается. Отдельный установщик Hysteria2 больше не поддерживается.
Сервер Hysteria2 продолжает работать как раньше, но не обновляется этим скриптом. Сервер Hysteria2 продолжает работать как раньше, но не обновляется этим скриптом.
Hysteria2 входит в основную установку 3X-UI KIT (один из 11 протоколов). Hysteria2 входит в основную установку 3X-UI_autoVPS (один из 11 протоколов).
Если нужен только Hysteria2 без панели – официальный установщик: Если нужен только Hysteria2 без панели – официальный установщик:
bash <(curl -fsSL https://get.hy2.sh/) bash <(curl -fsSL https://get.hy2.sh/)
MSG MSG
+637 -10
View File
@@ -64,7 +64,7 @@ kl_need_root() { [[ ${EUID:-0} -eq 0 ]] || kl_die "нужен root"; }
kit_local_help() { kit_local_help() {
cat <<'EOF' cat <<'EOF'
3X-UI KIT — дополнения форка (hacker07): 3X-UI_autoVPS — дополнения форка (hacker07):
Состояние Состояние
kit local эта справка kit local эта справка
@@ -73,6 +73,16 @@ 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 — одна и та же страница)
kit local site поставить случайный сайт из нашего набора (10 штук)
kit local site --list список сайтов с номерами
kit local site 7 поставить конкретный
kit local site --show какой сайт стоит сейчас
Защита сервера (поведение взято из StressOzz/vps-setup, сделано безопаснее) Защита сервера (поведение взято из StressOzz/vps-setup, сделано безопаснее)
kit local ssh 2222 добавить порт SSH (прежние продолжают работать) kit local ssh 2222 добавить порт SSH (прежние продолжают работать)
@@ -227,7 +237,7 @@ EOF
printf ' (dry-run) записал бы в %s: [Socket] + ListenStream=%s (старый порт остаётся)\n' "$KL_SOCKET_DROPIN" "$new" printf ' (dry-run) записал бы в %s: [Socket] + ListenStream=%s (старый порт остаётся)\n' "$KL_SOCKET_DROPIN" "$new"
else else
install -d -m 755 "$(dirname "$KL_SOCKET_DROPIN")" install -d -m 755 "$(dirname "$KL_SOCKET_DROPIN")"
printf '# 3X-UI KIT (форк): дополнительный порт SSH, добавлен %s\n[Socket]\nListenStream=%s\n' "$(date +%F)" "$new" >"$KL_SOCKET_DROPIN" printf '# 3X-UI_autoVPS (форк): дополнительный порт SSH, добавлен %s\n[Socket]\nListenStream=%s\n' "$(date +%F)" "$new" >"$KL_SOCKET_DROPIN"
kl_ok "добавлен $KL_SOCKET_DROPIN" kl_ok "добавлен $KL_SOCKET_DROPIN"
fi fi
kl_run systemctl daemon-reload kl_run systemctl daemon-reload
@@ -238,7 +248,7 @@ EOF
printf ' (dry-run) записал бы в %s: Port %s (старый порт остаётся)\n' "$KL_SSHD_DROPIN" "$new" printf ' (dry-run) записал бы в %s: Port %s (старый порт остаётся)\n' "$KL_SSHD_DROPIN" "$new"
else else
install -d -m 755 /etc/ssh/sshd_config.d install -d -m 755 /etc/ssh/sshd_config.d
printf '# 3X-UI KIT (форк): дополнительный порт SSH, добавлен %s\nPort %s\n' "$(date +%F)" "$new" >"$KL_SSHD_DROPIN" printf '# 3X-UI_autoVPS (форк): дополнительный порт SSH, добавлен %s\nPort %s\n' "$(date +%F)" "$new" >"$KL_SSHD_DROPIN"
kl_ok "добавлен $KL_SSHD_DROPIN" kl_ok "добавлен $KL_SSHD_DROPIN"
fi fi
kl_run sshd -t || { kl_warn "sshd -t не принял конфиг — убираю наш файл, ничего не перезапускаю"; kl_dry || rm -f "$KL_SSHD_DROPIN"; return 1; } kl_run sshd -t || { kl_warn "sshd -t не принял конфиг — убираю наш файл, ничего не перезапускаю"; kl_dry || rm -f "$KL_SSHD_DROPIN"; return 1; }
@@ -408,14 +418,13 @@ APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7"; APT::Periodic::AutocleanInterval "7";
EOF EOF
cat >/etc/apt/apt.conf.d/52-kit-local-upgrades <<'EOF' cat >/etc/apt/apt.conf.d/52-kit-local-upgrades <<'EOF'
// 3X-UI KIT (форк): только обновления безопасности, никакой самовольной перезагрузки. // 3X-UI_autoVPS (форк): только обновления безопасности, никакой самовольной перезагрузки.
Unattended-Upgrade::Automatic-Reboot "false"; Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
EOF EOF
systemctl enable --now unattended-upgrades >/dev/null 2>&1 || kl_warn "служба не включилась — проверьте: systemctl status unattended-upgrades" systemctl enable --now unattended-upgrades >/dev/null 2>&1 || kl_warn "служба не включилась — проверьте: systemctl status unattended-upgrades"
kl_ok "включено: обновления безопасности ставятся сами" # Одна строка: подробности нужны редко, а в выводе установки они превращались в шум.
kl_ok "панель и ядро это не затрагивает — они ставятся не из apt, а из своих релизов" kl_ok "автообновления безопасности включены (панель и ядро это не затрагивает, сервер сам не перезагружается)"
kl_warn "обновление ядра применится только после перезагрузки, сам сервер не перезагружается"
return 0 return 0
} }
@@ -436,12 +445,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
@@ -452,6 +463,432 @@ EOF
return 0 return 0
} }
# ------------------------------------------------------------------ заглушка: ИТ-сайты (наши)
# Десять правдоподобных ИТ-сайтов: хостинг, DevOps, мониторинг, разработка, безопасность,
# поддержка, сети и колокейшн, заметки про Linux, бэкапы, документация проекта.
# Ставится случайный (kit local site), конкретный — по номеру (kit local site 7).
# Страница отдаётся nginx на 443 при неизвестном SNI и синхронизируется в ответ Hysteria2
# (kit net masq on) — поэтому менять её нужно этой командой, иначе 443/tcp и UDP-порт начнут
# отвечать разными страницами (это заметно снаружи).
KL_SITE_FILE=/var/www/kit/index.html
KL_SITE_TITLES=(
"Radian Analytics — data platforms and BI"
"Circuit Nine — DevOps consulting"
"Cortex Monitor — server monitoring"
"Bluefin Software — custom development"
"Vantage Security — audits and penetration tests"
"Keystone IT — managed systems support"
"Meridian Networks — colocation and transit"
"Lumen Notes — Linux and networking"
"Evercore Backup — backup and recovery"
"Halyard Docs — project documentation"
)
KL_SITE_TOPICS=(
"дата-аналитика: хранилище, отчёты, дашборды"
"DevOps-консалтинг: CI/CD, Kubernetes, наблюдаемость"
"мониторинг серверов: метрики, алерты, статус-страница"
"разработка ПО на заказ: портфолио, стек, процесс"
"аудит и пентест: этапы, отчёт, повторная проверка"
"ИТ-поддержка: серверы, сети, рабочие места"
"колокейшн и транзит: стойки, каналы, BGP"
"заметки про Linux, nginx и сети"
"бэкапы и восстановление: политика, проверки, тесты"
"документация проекта: установка, конфигурация, справка"
)
KL_SITE_CSS=':root{--fg:#1b1f24;--mu:#5b6670;--bd:#e3e6ea;--ac:#1f6feb;--bg:#fff}
*{box-sizing:border-box}
body{margin:0;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);background:var(--bg)}
.wrap{max-width:980px;margin:0 auto;padding:0 20px}
header{border-bottom:1px solid var(--bd)}
header .wrap{display:flex;align-items:center;justify-content:space-between;height:64px}
.logo{font-weight:700;text-decoration:none;color:var(--fg)}
nav a{color:var(--mu);text-decoration:none;margin-left:18px;font-size:15px}
.hero{padding:72px 0 40px}
.hero h1{font-size:40px;line-height:1.15;margin:0 0 16px;letter-spacing:-.02em}
.hero p{font-size:18px;color:var(--mu);max-width:640px;margin:0 0 28px}
.btn{display:inline-block;background:var(--ac);color:#fff;text-decoration:none;padding:12px 22px;border-radius:6px;font-weight:600}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px;padding:8px 0 44px}
.card{border:1px solid var(--bd);border-radius:8px;padding:22px}
.card h3{margin:0 0 8px;font-size:18px}
.card p{margin:0;color:var(--mu)}
.prose{padding:0 0 56px;max-width:780px}
.prose h2{font-size:22px;margin:34px 0 10px}
.prose p,.prose li{color:var(--mu)}
code{background:#f4f6f8;padding:2px 5px;border-radius:4px;font-size:14px}
footer{border-top:1px solid var(--bd);padding:26px 0;color:var(--mu);font-size:14px}
table{border-collapse:collapse;width:100%}
th,td{border-bottom:1px solid var(--bd);text-align:left;padding:10px 8px;font-size:15px}
ul{padding-left:20px}
.modal{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(11,13,17,.55);padding:20px;z-index:50}
.modal.on{display:flex}
.modal .box{background:#fff;border-radius:10px;max-width:430px;padding:26px 26px 20px;box-shadow:0 18px 50px rgba(0,0,0,.28)}
.modal h4{margin:0 0 8px;font-size:19px}
.modal p{margin:0 0 18px;color:var(--mu)}
.modal button{background:var(--ac);color:#fff;border:0;border-radius:6px;padding:10px 18px;font:inherit;font-weight:600;cursor:pointer}
@media(max-width:600px){.hero{padding:44px 0 28px}.hero h1{font-size:29px}}'
kl_site_open() { # заголовок описание бренд «навигация как HTML»
cat <<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>$1</title>
<meta name="description" content="$2">
<meta name="generator" content="static">
<style>$KL_SITE_CSS</style>
</head>
<body>
<header><div class="wrap"><a class="logo" href="/">$3</a><nav>$4</nav></div></header>
HTML
}
kl_site_close() { # бренд
cat <<HTML
<footer><div class="wrap"><p>© $(date +%Y) $1 · <a href="#">Imprint</a> · <a href="#">Privacy</a> · <a href="#">Status</a></p></div></footer>
<!-- Ссылки и кнопки живые: открывают окно «раздел в разработке». Никаких внешних запросов. -->
<div class="modal" id="dev" role="dialog" aria-modal="true" aria-labelledby="devtitle">
<div class="box">
<h4 id="devtitle">This section is under construction</h4>
<p>We are still building this part of the site. We are sorry for the inconvenience — it will be
available shortly.</p>
<button type="button" id="devok">Got it</button>
</div>
</div>
<script>
(function () {
var m = document.getElementById('dev');
if (!m) return;
function show(e) { if (e) e.preventDefault(); m.classList.add('on'); document.body.style.overflow = 'hidden'; }
function hide() { m.classList.remove('on'); document.body.style.overflow = ''; }
var links = document.querySelectorAll('a[href^="#"], a.btn, nav a, footer a');
for (var i = 0; i < links.length; i++) { links[i].addEventListener('click', show); }
m.addEventListener('click', function (e) { if (e.target === m) hide(); });
document.getElementById('devok').addEventListener('click', hide);
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') hide(); });
})();
</script>
</body>
</html>
HTML
}
kl_site_html() { # номер (с нуля) → страница на stdout
case $1 in
0) # дата-аналитика
kl_site_open "Radian Analytics — data platforms and BI" "We collect data from accounting, warehouses and websites into one place and build the few reports the business actually reads." "Radian Analytics" '<a href="#services">Services</a><a href="#approach">Approach</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Reports that arrive before the meeting</h1>
<p>We pull data out of your accounting system, warehouse and website, load it into one warehouse, and build the handful of dashboards people actually open.</p>
<a class="btn" href="#services">What we build</a>
</div></section>
<section id="services" class="wrap cards">
<div class="card"><h3>Data pipelines</h3><p>Scheduled loads from databases, APIs and spreadsheets, with checks that catch a broken source early.</p></div>
<div class="card"><h3>Warehouse</h3><p>PostgreSQL or ClickHouse, modelled so that a new question does not need a new table every time.</p></div>
<div class="card"><h3>Dashboards</h3><p>The five numbers that matter on one page, with the detail one click behind each of them.</p></div>
</section>
<section id="approach" class="wrap prose">
<h2>How it usually goes</h2>
<p>Two weeks to collect the sources and agree on definitions — most arguments turn out to be about definitions, not about software. Then the pipeline runs nightly and the first reports go out a week later.</p>
<h2>One warning</h2>
<p>A dashboard nobody opens is expensive decoration. One report that somebody uses beats ten that nobody does.</p>
</section>
</main>
HTML
kl_site_close "Nordhost" ;;
1) # DevOps-консалтинг
kl_site_open "Circuit Nine — DevOps consulting" "We help small teams ship safely: CI/CD, Kubernetes, infrastructure as code and observability." "Circuit Nine" '<a href="#work">Work</a><a href="#approach">Approach</a><a href="#about">About</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Ship on Fridays without holding your breath</h1>
<p>We set up pipelines, infrastructure and monitoring so that releasing is a boring, repeatable event — and so that on-call does not ruin anyone’s week.</p>
<a class="btn" href="#work">What we do</a>
</div></section>
<section id="work" class="wrap cards">
<div class="card"><h3>CI/CD</h3><p>Build pipelines, artefact registries, staging environments and one-command rollbacks.</p></div>
<div class="card"><h3>Infrastructure</h3><p>Terraform and Ansible for servers and cloud, with reviews and tests in the pipeline.</p></div>
<div class="card"><h3>Observability</h3><p>Metrics, logs and traces wired together, with alerts that people actually trust.</p></div>
</section>
<section id="approach" class="wrap prose">
<h2>How we work</h2>
<p>A two-week discovery, then small changes with a review after each one. Everything lands in your repository and your accounts — no private tooling, no lock-in.</p>
<table>
<tr><th>Week</th><th>What happens</th></tr>
<tr><td>1–2</td><td>Audit of the current setup, a written report with priorities</td></tr>
<tr><td>3–6</td><td>Pipelines and infrastructure, pairing with your engineers</td></tr>
<tr><td>7–8</td><td>Monitoring, runbooks and a handover session</td></tr>
</table>
</section>
</main>
HTML
kl_site_close "Circuit Nine" ;;
2) # мониторинг
kl_site_open "Cortex Monitor — server monitoring" "Uptime, resource and certificate monitoring with alerting to Telegram, e-mail and webhooks." "Cortex Monitor" '<a href="#checks">Checks</a><a href="#alerts">Alerts</a><a href="#pricing">Pricing</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Know about the outage before your users do</h1>
<p>HTTP, TCP, ICMP and TLS checks from six locations, host metrics through a lightweight agent, and alerts that reach you in seconds.</p>
<a class="btn" href="#checks">See checks</a>
</div></section>
<section id="checks" class="wrap cards">
<div class="card"><h3>Availability</h3><p>HTTP and TCP checks with keyword matching, redirects and status codes.</p></div>
<div class="card"><h3>Resources</h3><p>CPU, memory, disk and network of every host on one page.</p></div>
<div class="card"><h3>Certificates</h3><p>Expiry warnings 30, 14 and 3 days ahead, with a list of what needs renewing.</p></div>
</section>
<section id="alerts" class="wrap prose">
<h2>Alerting</h2>
<p>Alerts are grouped so that a single broken router does not produce forty messages. Quiet hours, maintenance windows and escalation to a second contact are configurable per check.</p>
<h2>Maybe you do not need us yet</h2>
<p>If you run one server, an uptime check and a cron job with certificate renewal will do. Come back when there are more of them than you can remember.</p>
</section>
</main>
HTML
kl_site_close "Cortex Monitor" ;;
3) # разработка ПО
kl_site_open "Bluefin Software — custom development" "A small team building web services, integrations and internal tools for companies that grew out of spreadsheets." "Bluefin Software" '<a href="#services">Services</a><a href="#stack">Stack</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Software for the part of the business nobody sells</h1>
<p>Integrations, back-office tools and web services. We take a process that lives in three spreadsheets and one person’s head, and give it a proper home.</p>
<a class="btn" href="#services">Services</a>
</div></section>
<section id="services" class="wrap cards">
<div class="card"><h3>Web services</h3><p>APIs and admin panels in Python or Go, deployed to your infrastructure.</p></div>
<div class="card"><h3>Integrations</h3><p>Accounting, warehouses, CRM and everything that has an API or a database.</p></div>
<div class="card"><h3>Rescue work</h3><p>Taking over an unfinished project: audit, plan, tidy up, finish.</p></div>
</section>
<section id="stack" class="wrap prose">
<h2>Stack</h2>
<p>Python (Django, FastAPI), PostgreSQL, Redis, Docker, and whatever the customer already runs. We keep deployments simple enough that a new colleague can understand them in a day.</p>
<h2>Team</h2>
<p>Four engineers and a part-time designer. We take on three projects at a time so that nobody is queued behind somebody else’s deadline.</p>
</section>
</main>
HTML
kl_site_close "Bluefin Software" ;;
4) # безопасность
kl_site_open "Vantage Security — audits and penetration tests" "External and internal penetration tests, configuration reviews and help with fixing what we find." "Vantage Security" '<a href="#services">Services</a><a href="#process">Process</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Find the problems before somebody else does</h1>
<p>Penetration tests and configuration reviews for web services, networks and cloud accounts — with a report your engineers can act on and a retest when the fixes are in.</p>
<a class="btn" href="#services">Services</a>
</div></section>
<section id="services" class="wrap cards">
<div class="card"><h3>External test</h3><p>Everything reachable from the internet: services, TLS, DNS, mail.</p></div>
<div class="card"><h3>Internal test</h3><p>What an attacker does after getting a foothold in an office or a VPN.</p></div>
<div class="card"><h3>Review</h3><p>Settings, backups, access rights and monitoring — usually the cheapest findings.</p></div>
</section>
<section id="process" class="wrap prose">
<h2>Process</h2>
<p>Scope, rules of engagement and a test window are agreed in writing first. We work against a staging copy where possible, and we never touch production data beyond what the scope allows.</p>
<h2>Report</h2>
<p>Findings are ranked by what they let an attacker do, with reproduction steps and concrete fixes. A retest of the fixed issues is included in the price.</p>
</section>
</main>
HTML
kl_site_close "Vantage Security" ;;
5) # ИТ-поддержка
kl_site_open "Keystone IT — managed systems support" "We look after servers, networks, backups and workstations for offices of ten to two hundred people." "Keystone IT" '<a href="#services">Services</a><a href="#support">Support</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>An IT department, without hiring one</h1>
<p>Monitoring, updates, backups and a real person on the phone. Contracts start at a fixed monthly fee, and you can leave with thirty days’ notice.</p>
<a class="btn" href="#services">What is included</a>
</div></section>
<section id="services" class="wrap cards">
<div class="card"><h3>Servers</h3><p>Updates, monitoring, disk space and certificate renewal handled for you.</p></div>
<div class="card"><h3>Backups</h3><p>Nightly copies, offsite storage and a restore test twice a year.</p></div>
<div class="card"><h3>Workplaces</h3><p>Accounts, laptops, printers and the printer driver that never works.</p></div>
</section>
<section id="support" class="wrap prose">
<h2>Support</h2>
<p>Weekdays from 8 to 19, with an on-call line for outages. Response time for a full outage is one hour, for ordinary requests one working day.</p>
<p>We keep a written inventory of your systems, so that a new engineer — ours or yours — can take over without a two-week interview with the person who built it.</p>
</section>
</main>
HTML
kl_site_close "Keystone IT" ;;
6) # сети и колокейшн
kl_site_open "Meridian Networks — colocation and transit" "Rack space, cross-connects and IP transit in two carrier-neutral facilities." "Meridian Networks" '<a href="#colo">Colocation</a><a href="#transit">Transit</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Racks, cross-connects and transit</h1>
<p>Quarter, half and full racks in Frankfurt and Amsterdam, with remote hands, redundant power and a choice of transit providers.</p>
<a class="btn" href="#colo">Colocation</a>
</div></section>
<section id="colo" class="wrap cards">
<div class="card"><h3>Quarter rack</h3><p>10 U, 2 kW, 1 Gbps port and one IPv4 prefix.</p></div>
<div class="card"><h3>Half rack</h3><p>22 U, 4 kW and a redundant 10 Gbps pair.</p></div>
<div class="card"><h3>Private cage</h3><p>From eight racks, with your own access control and monitoring.</p></div>
</section>
<section id="transit" class="wrap prose">
<h2>Transit and peering</h2>
<p>We announce your prefix over two upstreams and the local internet exchange. BGP communities for traffic engineering, RPKI filtering and a looking glass are available to customers.</p>
<table>
<tr><th>Port</th><th>Committed rate</th></tr>
<tr><td>1 Gbps</td><td>from 50 Mbps</td></tr>
<tr><td>10 Gbps</td><td>from 1 Gbps</td></tr>
<tr><td>2×10 Gbps</td><td>from 2 Gbps, redundant</td></tr>
</table>
</section>
</main>
HTML
kl_site_close "Meridian Networks" ;;
7) # блог
kl_site_open "Lumen Notes — Linux and networking" "Short notes about servers, nginx, storage and networks — written while working, mostly for myself." "Lumen Notes" '<a href="#posts">Posts</a><a href="#about">About</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Notes from a server room</h1>
<p>Things I worked out once and do not want to work out again: nginx, systemd, ZFS, routing and the odd shell trick.</p>
<a class="btn" href="#posts">Read the posts</a>
</div></section>
<section id="posts" class="wrap prose">
<h2>Replacing a certificate without dropping connections</h2>
<p>Reload, not restart. <code>systemctl reload nginx</code> keeps the listeners open and swaps the configuration, so long downloads and websockets survive. A restart does not.</p>
<h2>Finding what filled the disk</h2>
<p><code>du -xh --max-depth=1 / | sort -h</code> first, then follow the biggest branch. Journals under <code>/var/log/journal</code> are a common answer; <code>journalctl --vacuum-size=200M</code> trims them.</p>
<h2>Checking that a port is really listening</h2>
<p><code>ss -ltnp</code> shows TCP listeners and the process behind each one. If something answers on 443 but is not in the list, look for a proxy in front of it.</p>
<h2>Backup is not a copy</h2>
<p>A copy on the same disk protects against nothing but a fat finger. The test that matters is a restore, done on purpose, on a machine that is not the original.</p>
</section>
</main>
HTML
kl_site_close "Lumen Notes" ;;
8) # бэкапы
kl_site_open "Evercore Backup — backup and recovery" "Offsite backups with a documented restore procedure and a restore test every quarter." "Evercore Backup" '<a href="#policy">Policy</a><a href="#restore">Restore</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>A backup you have never restored is a rumour</h1>
<p>We copy your servers and databases to a separate location, keep versions, and prove once a quarter that a restore actually works.</p>
<a class="btn" href="#policy">How it works</a>
</div></section>
<section id="policy" class="wrap cards">
<div class="card"><h3>Nightly copies</h3><p>Files and databases, encrypted before they leave the server.</p></div>
<div class="card"><h3>Versions</h3><p>Hourly for a day, daily for a month, monthly for a year.</p></div>
<div class="card"><h3>Restore test</h3><p>A quarterly restore into a clean machine, with a short report.</p></div>
</section>
<section id="restore" class="wrap prose">
<h2>Recovery</h2>
<p>A lost file takes minutes: we restore the version you pick. A lost server takes an hour or two: a prepared image plus your latest copy, booted and checked before it goes back into service.</p>
<p>Encryption keys are kept by you as well as by us, so that neither side alone can read the copies.</p>
</section>
</main>
HTML
kl_site_close "Evercore Backup" ;;
9) # документация проекта
kl_site_open "Halyard Docs — project documentation" "Installation, configuration and reference for a self-hosted service: everything in one place." "Halyard Docs" '<a href="#install">Install</a><a href="#config">Config</a><a href="#reference">Reference</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Documentation</h1>
<p>Installation, configuration and the reference pages. Written for the current release; older versions keep their own section.</p>
<a class="btn" href="#install">Start here</a>
</div></section>
<section id="install" class="wrap prose">
<h2>Installation</h2>
<p>Download the archive, unpack it into <code>/opt/halyard</code> and run the setup script. The script asks three questions and writes a configuration file next to the binary.</p>
<h2>Configuration</h2>
<p>Settings live in <code>/etc/halyard/config.toml</code>. Every option has a default, so a minimal file is often enough. Changes are applied with <code>halyard reload</code> — no restart, no dropped connections.</p>
</section>
<section id="reference" class="wrap prose">
<h2>Reference</h2>
<table>
<tr><th>Command</th><th>What it does</th></tr>
<tr><td><code>halyard status</code></td><td>Shows the running version and the state of each component</td></tr>
<tr><td><code>halyard check</code></td><td>Validates the configuration and the listening ports</td></tr>
<tr><td><code>halyard logs</code></td><td>Follows the log with readable timestamps</td></tr>
</table>
<p>Questions and patches are welcome through the issue tracker; the address is on the contact page.</p>
</section>
</main>
HTML
kl_site_close "Halyard Docs" ;;
*) kl_die "нет такого сайта: $1" ;;
esac
}
kl_site_current() { # какой из наших сайтов стоит сейчас (пусто — не наш)
local i t
[[ -s $KL_SITE_FILE ]] || return 0
for i in "${!KL_SITE_TITLES[@]}"; do
t=${KL_SITE_TITLES[i]}
if grep -qF "<title>$t</title>" "$KL_SITE_FILE" 2>/dev/null; then
printf '%s' "$((i + 1)). $t"
return 0
fi
done
return 0
}
kit_local_site() {
kl_args "$@"
local arg=${KL_REST[0]:-} n cur
case $arg in
--list | list)
kl_say "сайты-заглушки в наборе (ставится случайный)"
local i
for i in "${!KL_SITE_TITLES[@]}"; do
printf ' %2d. %-22s %s\n' "$((i + 1))" "${KL_SITE_TITLES[i]%% — *}" "${KL_SITE_TOPICS[i]}"
done
printf ' %s\n' "поставить конкретный: kit local site 7"
return 0 ;;
--show | show | status)
cur=$(kl_site_current)
if [[ -n $cur ]]; then kl_ok "сейчас стоит наш сайт: $cur"
elif [[ -s $KL_SITE_FILE ]]; then kl_ok "сейчас стоит страница автора (случайная из его наборов)"
else kl_warn "файла $KL_SITE_FILE нет"; fi
return 0 ;;
"" | random | --random) n=$((RANDOM % ${#KL_SITE_TITLES[@]})) ;;
*)
[[ $arg =~ ^[0-9]{1,2}$ ]] && ((10#$arg >= 1 && 10#$arg <= ${#KL_SITE_TITLES[@]})) \
|| { kl_die "kit local site [номер 1..${#KL_SITE_TITLES[@]}|--list|--show]"; return 1; }
n=$((10#$arg - 1)) ;;
esac
if kl_dry; then
kl_say "заглушка: ${KL_SITE_TITLES[n]} (${KL_SITE_TOPICS[n]})"
printf ' (dry-run) записал бы %s и синхронизировал бы ответ Hysteria2 (kit net masq on)\n' "$KL_SITE_FILE"
return 0
fi
kl_need_root || return 1
kl_say "заглушка: ${KL_SITE_TITLES[n]} (${KL_SITE_TOPICS[n]})"
install -d -m 755 /var/www/kit
[[ -f $KL_SITE_FILE ]] && kl_backup "$KL_SITE_FILE"
if ! kl_site_html "$n" >"$KL_SITE_FILE.new"; then
rm -f "$KL_SITE_FILE.new"; kl_die "не удалось собрать страницу"; return 1
fi
chmod 644 "$KL_SITE_FILE.new" && mv "$KL_SITE_FILE.new" "$KL_SITE_FILE" \
|| { rm -f "$KL_SITE_FILE.new"; kl_die "не удалось заменить $KL_SITE_FILE"; return 1; }
kl_ok "страница обновлена: $KL_SITE_FILE ($(wc -c <"$KL_SITE_FILE") байт)"
# Порт 443 отдаёт файл через nginx (перезагрузка не нужна). UDP-порт Hysteria2 держит копию
# страницы внутри подключения — её надо обновить, иначе ответы разойдутся.
if kl_have kit; 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 отвечает той же страницей"
else
kl_warn "Hysteria2 не синхронизирован (может, его нет в этой установке) — проверьте: kit net masq on"
fi
fi
kl_ok "посмотреть снаружи: curl -k https://<IP-или-домен>/ ; состояние: kit local site --show, kit check"
}
kit_local_cmd() { kit_local_cmd() {
case ${1:-help} in case ${1:-help} in
help | -h | --help) kit_local_help ;; help | -h | --help) kit_local_help ;;
@@ -464,6 +901,11 @@ 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 "$@" ;;
*) *)
printf 'Неизвестная команда форка: %s\n' "$1" >&2 printf 'Неизвестная команда форка: %s\n' "$1" >&2
kit_local_help >&2 kit_local_help >&2
@@ -474,6 +916,186 @@ 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 '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt 2>/dev/null; then
grep -aE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt | sed 's/^[[:space:]]*/ /'
else
kl_warn "данных панели в /root/3x-ui.txt нет"
fi
printf '\n%s\n' "${B:-}Сертификат панели${N:-}"
if [[ -s /root/cert/ip/fullchain.pem ]] && openssl x509 -in /root/cert/ip/fullchain.pem -noout -issuer 2>/dev/null | grep -q "Let's Encrypt"; then
printf ' Let'"'"'s Encrypt на IP — доверенный, браузер не ругается\n'
elif [[ -s /root/cert/ip/fullchain.pem ]]; then
printf ' самоподписанный — браузер предупредит, подписка в приложениях может не открыться\n'
else
printf ' сертификата на IP нет: панель открывается только через SSH-туннель\n'
fi
till2=$(grep -rhoE 'retry after [0-9-]+ [0-9:]+ UTC' /var/log/3x-ui-install.log "$HOME"/.acme.sh/*/ca 2>/dev/null | tail -1 | sed 's/^retry after //')
[[ -n $till2 ]] && printf ' лимит Let'"'"'s Encrypt: повтор выпуска возможен после %s (затем запустите установщик снова)\n' "$till2"
printf '\n%s\n' "${B:-}Ссылки на подключения и QR-код${N:-}"
if [[ -z $u ]]; then u=$(kit user list 2>/dev/null | awk 'NR > 1 && NF { print $1; exit }'); u=${u:-admin}; fi
local now; now=$(tr '\n' ' ' </etc/kit/kl-protos 2>/dev/null || true)
if [[ $(wc -w <<<"$now") -gt 1 ]]; then kit user link "$u" --all; else kit user link "$u"; fi
printf '\n%s\n' "${B:-}Заглушка (что видит посторонний, открыв адрес сервера)${N:-}"
kit local site --show
printf ' посмотреть снаружи: откройте https://<IP сервера>/ в браузере или curl -k https://<IP>/
'
}
kit_local_links() { # [имя] — подписка, QR-код и ссылки по протоколам (то же, что «kit → Пользователи → Показать ссылки»)
local u=${1:-} now
if [[ -z $u ]]; then
u=$(kit user list 2>/dev/null | awk 'NR > 1 && NF { print $1; exit }')
u=${u:-admin}
fi
printf ' пользователь: %s\n' "$u"
now=$(tr '\n' ' ' </etc/kit/kl-protos 2>/dev/null || true)
# Ссылки на каждый протокол — только если протоколов больше одного; при одном достаточно подписки.
if [[ $(wc -w <<<"$now") -gt 1 ]]; then kit user link "$u" --all; else kit user link "$u"; fi
}
kit_local_uninstall() {
# Наш шов: логику удаления отдаём авторской команде «kit uninstall». Она снимает и сам kit, чтобы на
# сервере не осталось следов, поэтому своя реализация здесь больше не нужна — команда оставлена
# синонимом, чтобы привычное «kit local uninstall» продолжало работать.
local a=() x
for x in "$@"; do
case $x in
--dry-run | --yes | --no-backup) a+=("$x") ;;
--help | -h)
printf 'kit local uninstall [--dry-run] [--yes] [--no-backup] — то же, что kit uninstall\n'
return 0 ;;
*) kl_die "kit local uninstall [--dry-run] [--yes] [--no-backup]"; return 1 ;;
esac
done
[[ -x /usr/local/bin/kit ]] || { kl_die "не нашёл /usr/local/bin/kit — удалить вручную: kit uninstall"; return 1; }
printf ' удаляю по авторской логике: /usr/local/bin/kit uninstall %s\n' "${a[*]}"
exec /usr/local/bin/kit uninstall "${a[@]}"
}
# ---------------------------------------------------------------- полная справка `kit help`
# Категории и все команды: сначала авторские, затем наши. Если автор добавит команду, а справка
# про неё не знает — сборку релиза остановит проверка tools/check-help.sh.
kit_help_all() {
cat <<EOF
${B:-}3X-UI_autoVPS ${KIT_VERSION:-?} — все команды${N:-} (то же самое доступно в меню: kit без аргументов)
Состояние и версии
kit меню
kit version версии: сборка, панель 3X-UI, ядро Xray
kit local version наша сборка, источник обновлений, дата файла
kit local channel откуда берутся обновления и чем проверяется подпись
kit local seams какие места форк правит в файлах автора
kit help эта справка
Пользователи
kit user add <имя> [--gb N] [--days N] [--devices N] создать (пусто/0 — без ограничения)
kit user list список пользователей
kit user link <имя> [--all | --amnezia | --telegram] подписка с QR-кодом и ссылки
kit user limit <имя> --gb N --days N --devices N лимиты трафика, срока, устройств
kit user on <имя> включить пользователя
kit user off <имя> выключить пользователя
kit user del <имя> удалить
kit user enforce применить лимиты сейчас
kit user vision <имя> [on|off] поток xtls-rprx-vision у REALITY
Подключения и порты
kit net состояние подключений
kit net on <имя> включить подключение
kit net off <имя> выключить подключение
kit net port <имя> <порт> перевести подключение на другой порт
kit net vision on|off Vision у всех REALITY сразу
kit net split on|off|check|apply разделение трафика по правилам (split)
kit port какие порты слушают подключения
kit port set <имя> <порт> сменить порт подключения
kit reality add [порт] добавить второй REALITY
Маскировка и сайт
kit net site [сайт] сменить сайт маскировки (без аргумента — подобрать соседа по подсети)
kit net fp <отпечаток> отпечаток TLS (chrome, firefox, safari, …)
kit sni какие сайты маскировки у подключений
kit sni rotate сменить сайты маскировки у всех подключений
kit local masking состояние маскировки (у REALITY должен быть свой домен)
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 fix [--dry-run] безопасные починки: время, службы, конфиги
kit local hardening сводка по защите и что ещё можно включить
Обслуживание и обновление
kit update [--panel | --force | --auto | --manual] обновить сборку, панель, автообновление
kit panel update обновить панель 3X-UI
kit backup копия сервера: панель, сертификаты, настройки
kit net masq on|off ответ Hysteria2 страницей сайта
kit net dns on|off DoH в подписках
Защита сервера (наши команды)
kit local ssh <порт> | --status | --close-old <порт> добавить порт SSH, какие слушают, закрыть прежние
kit local ping off | on | status ответ на ping (правилом ufw)
kit local passwd [--generate] [пользователь] сменить пароль (в логи не попадает)
kit local upgrades on | off | status обновления безопасности
Общие правила
* у наших команд есть --dry-run: показывает план и ничего не меняет
* подробная справка по нашему слою: kit local help
* копии изменённых файлов: /root/kit-local-backup-<дата>/
EOF
}
kit_local_dispatch() { kit_local_dispatch() {
case ${1:-} in case ${1:-} in
local) local)
@@ -481,6 +1103,11 @@ kit_local_dispatch() {
kit_local_cmd "$@" kit_local_cmd "$@"
exit $? exit $?
;; ;;
help | -h | --help)
# Полная справка: авторские команды + наши, по категориям.
kit_help_all
exit 0
;;
*) return 0 ;; *) return 0 ;;
esac esac
} }
+55 -12
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):
@@ -494,11 +536,11 @@ def routing_payload(kind, loaded):
else: else:
ips.append(v) ips.append(v)
if kind == "happ": if kind == "happ":
data = {"Name": "3X-UI KIT", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites, data = {"Name": "3X-UI_autoVPS", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites,
"ProxyIp": ips, "DirectIp": ["geoip:private"]} "ProxyIp": ips, "DirectIp": ["geoip:private"]}
else: else:
# localhost – системный DNS клиента для прямых сайтов (как в подписке Mihomo); список direct_dns его заменяет. # localhost – системный DNS клиента для прямых сайтов (как в подписке Mihomo); список direct_dns его заменяет.
data = {"Name": "3X-UI KIT", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct", data = {"Name": "3X-UI_autoVPS", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct",
"ProxySites": sites, "ProxyIp": ["1.1.1.1", "8.8.8.8"] + ips, "DirectSites": ["regexp:.*"], "ProxySites": sites, "ProxyIp": ["1.1.1.1", "8.8.8.8"] + ips, "DirectSites": ["regexp:.*"],
"DirectIp": ["geoip:private", "0.0.0.0/0", "::/0"], "DomesticDNSDomain": direct_dns[0] if direct_dns else "localhost"} "DirectIp": ["geoip:private", "0.0.0.0/0", "::/0"], "DomesticDNSDomain": direct_dns[0] if direct_dns else "localhost"}
text = json.dumps(data, ensure_ascii=False, separators=(",", ":")) text = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
@@ -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,9 +613,10 @@ 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()
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+913 -130
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Справка `kit help` обязана перечислять все команды: авторские (их берём прямо из диспетчера
# scripts/kit.sh) и наши. Если автор добавит команду, а мы забудем про справку — проверка упадёт,
# и сборка релиза остановится: так справка не отстанет молча.
set -Eeuo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
help=$(awk '/^kit_help_all\(\) \{/,/^\}/' scripts/kit-local.sh)
[[ -n $help ]] || { echo "✗ не нашёл kit_help_all в scripts/kit-local.sh" >&2; exit 1; }
bad=0
# 1) авторские команды: первое слово каждой ветки диспетчера kit.sh
while read -r word; do
[[ -n $word ]] || continue
case $word in __* | local | help | -h | --help | -v | --version) continue ;; esac
# «user off» в справке может стоять как «user on|off» — поэтому пробел допускаем и как разделитель вариантов
pat=${word// /[ |]}
grep -qE " kit ${pat}" <<<"$help" || { echo " ✗ в справке нет авторской команды: kit $word" >&2; bad=1; }
done < <(grep -oE '^ +"[a-z][a-z0-9 -]*"\)|^ +"[a-z][a-z0-9 -]*" \*\)' scripts/kit.sh | sed 's/)$//' | tr -d '"' | sed 's/^ *//' | sort -u)
# 2) наши команды из ветки kit_local_cmd
while read -r word; do
[[ -n $word ]] || continue
grep -qF " kit local $word" <<<"$help" || { echo " ✗ в справке нет нашей команды: kit local $word" >&2; bad=1; }
done < <(awk '/^kit_local_cmd\(\) \{/,/^\}/' scripts/kit-local.sh | grep -oE '^ [a-z-]+' | tr -d ' ' | sort -u)
if (( bad )); then
echo "✗ справка kit help отстала от набора команд — поправьте kit_help_all в scripts/kit-local.sh" >&2
exit 1
fi
echo " ✓ справка kit help содержит все команды (авторские и наши)"
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Проверка: подсказка «Выбор [0-N]» в каждом меню kit должна совпадать с числом пунктов.
Пункты считаются по строкам box (с учётом переносов через «\\»). Смысл проверки: мы добавляем в меню
свои пункты — однажды подсказка осталась «0-10» при 11 пунктах, и человек не мог выбрать последний.
"""
import re
import sys
import pathlib
path = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "scripts/kit.sh")
text = path.read_text(encoding="utf-8").replace("\\\n", "")
lines = text.splitlines()
bad = 0
for i, line in enumerate(lines):
m = re.search(r'\bbox\s+"([^"]+)"(.*)$', line)
if not m:
continue
items = re.findall(r'"(\d+)\.\s', m.group(2))
maxn = max((int(x) for x in items), default=0)
prompt = None
for j in range(i + 1, min(i + 120, len(lines))):
pm = re.search(r'ask_num\s+"Выбор \[0-(\d+)\]', lines[j])
if pm:
prompt = int(pm.group(1))
break
if prompt != maxn:
print(f"! меню «{m.group(1)}»: пунктов {maxn}, а подсказка «Выбор [0-{prompt}]»", file=sys.stderr)
bad += 1
print(f"меню проверены: {len(lines) and sum(1 for l in lines if 'box \"' in l)} · рассинхронов: {bad}")
sys.exit(1 if bad else 0)
+1 -1
View File
@@ -1 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI KIT fork releases (hacker07) ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
# Робот ежесуточной синхронизации: проверяет обновления у автора проекта и, если они есть,
# переносит их в нашу ветку dev — но только когда всё сошлось.
#
# Где живёт: запускается из .gitea/workflows/fork-sync.yml на раннере (CT 114 «runner»).
# Что НЕ делает: не выпускает релиз и не подписывает его — закрытый ключ подписи остаётся
# только на рабочей станции владельца, поэтому версия автора публикуется вручную.
#
# Переменные окружения (все имеют значения по умолчанию, кроме TOKEN):
# TOKEN токен Gitea с правом записи в репозиторий (берётся из секретов workflow)
# GITEA_URL внутренний адрес Gitea (снаружи контейнер её не видит: имя резолвится в публичный IP)
# REPO владелец/репозиторий
# UPSTREAM адрес авторского репозитория
# WORK рабочая копия на раннере
# BRANCH наша ветка для синхронизации
# LOG файл отчёта последнего запуска (его читают из контейнера руками)
set -Eeuo pipefail
GITEA_URL=${GITEA_URL:-http://10.10.10.54:3000}
REPO=${REPO:-hacker07/3X-UI_autoVPS}
UPSTREAM=${UPSTREAM:-https://github.com/itsnotkubrick/3X-UI_KIT}
WORK=${WORK:-/opt/kit-fork-sync/repo}
BRANCH=${BRANCH:-dev}
LOG=${LOG:-/opt/kit-fork-sync/last-run.log}
TOKEN=${TOKEN:-}
say() { printf '\n==> %s\n' "$*"; }
ok() { printf ' ✓ %s\n' "$*"; }
die() { printf '✗ %s\n' "$*" >&2; exit 1; }
mkdir -p "$(dirname "$LOG")"
exec > >(tee -a "$LOG") 2>&1
printf '\n===== запуск %s =====\n' "$(date '+%F %T %Z')"
[[ -n $TOKEN ]] || die "нет TOKEN: workflow не передал секрет GITEA_TOKEN"
command -v git >/dev/null || die "нет git"
PY=${PY:-$(command -v python3 || command -v python || true)}
[[ -n $PY ]] || die "нет python3 — без него не применить швы"
auth_url="${GITEA_URL}/${REPO}.git"
auth_url="${auth_url/http:\/\//http://x-access-token:${TOKEN}@}"
# ---------------------------------------------------------------- рабочая копия
if [[ ! -d $WORK/.git ]]; then
say "Первая синхронизация: клонирую репозиторий"
git clone -q "$auth_url" "$WORK" || die "не смог склонировать $REPO"
git -C "$WORK" remote set-url origin "$auth_url"
git -C "$WORK" remote add upstream "$UPSTREAM" 2>/dev/null || git -C "$WORK" remote set-url upstream "$UPSTREAM"
else
git -C "$WORK" remote set-url origin "$auth_url"
git -C "$WORK" remote set-url upstream "$UPSTREAM" 2>/dev/null || git -C "$WORK" remote add upstream "$UPSTREAM"
fi
cd "$WORK"
say "Забираю ветки: наша $BRANCH и авторскую main"
git fetch -q origin --prune
git fetch -q upstream 'refs/heads/main:refs/heads/upstream' --prune
git checkout -q "$BRANCH"
git reset -q --hard "origin/$BRANCH"
author_version=$(git show upstream:VERSION 2>/dev/null | tr -d '[:space:]' || true)
[[ -n $author_version ]] || die "у автора не читается VERSION — проверьте ветку upstream"
ours=$(tr -d '[:space:]' <VERSION)
echo " у автора: $author_version · у нас: $ours"
new_commits=$(git rev-list --count "$BRANCH..upstream")
if [[ $new_commits == 0 ]]; then
say "У автора нет новых коммитов — публиковать нечего"
ok "проверка выполнена, ветка $BRANCH не изменялась"
exit 0
fi
say "У автора $new_commits новых коммитов — переношу наши правки на свежий код"
git --no-pager log --oneline "$BRANCH..upstream" | sed 's/^/ /'
# ---------------------------------------------------------------- перенос и проверки
# Скрипт форка делает перенос (rebase на ветку автора) и все проверки: швы, синтаксис, LF, тесты.
# Без --apply: швы в нашей ветке уже накатаны, повторный накат на такое дерево падает (якоря автора
# заменены нашими строками). Номер версии поднимаем сами — по той же логике, что и в fork-sync.sh.
if ! bash tools/fork-sync.sh; then
die "перенос не прошёл (конфликт в местах наших правок). Ветка $BRANCH не изменена. Разберите вручную на рабочей станции"
fi
# Номер версии: у автора 1.3 → наш 1.3.1; если база та же, а мы дорабатываем — 1.3.2. Обязан расти.
base=${author_version%%.*}
cur=$(tr -d '[:space:]' <VERSION)
if [[ ${cur%%.*} == "$base" ]]; then
new_version="$base.$(( ${cur##*.} + 1 ))"
else
new_version="${author_version}.1"
fi
printf '%s\n' "$new_version" >VERSION
for f in scripts/kit.sh scripts/3x-ui.sh scripts/hysteria2.sh scripts/3x-ui-local.sh; do
sed -i "s/^KIT_VERSION=\"[0-9.]*\"/KIT_VERSION=\"$new_version\"/" "$f"
done
"$PY" tools/fork_apply.py >/dev/null 2>&1 || true # на дереве со швами это ожидаемо: якорей уже нет
echo " версия сборки: $cur → $new_version"
# Заглушка записи в нашем журнале версий: без раздела «## v<версия>» сборщик релиза откажется
# подписывать. Текст — на владельца, он правит его перед выпуском.
if ! grep -q "^## v$new_version" FORK-CHANGELOG.md; then
say "Добавляю запись в FORK-CHANGELOG для версии $new_version"
"$PY" - "$new_version" "$author_version" <<'PY'
import sys, pathlib
new, author = sys.argv[1], sys.argv[2]
p = pathlib.Path("FORK-CHANGELOG.md")
text = p.read_text(encoding="utf-8")
block = (f"## v{new}: обновление от автора {author}\n\n"
f"* Автоматическая синхронизация подтянула код автора {author} в нашу сборку: наши правки (швы)\n"
f" перенесены на свежий код, проверки прошли. Описать, что изменилось у автора, до выпуска релиза.\n\n")
marker = "## v"
i = text.find(marker)
p.write_text(text[:i] + block + text[i:], encoding="utf-8", newline="\n")
print(f" ✓ раздел «## v{new}» добавлен")
PY
fi
# Финальный контроль: швы на месте, синтаксис и LF — как того требует сборщик релиза.
"$PY" tools/fork_apply.py --check >/dev/null || die "после переноса швы не проверяются — ничего не публикую"
for f in scripts/*.sh tools/*.sh; do bash -n "$f" || die "$f: синтаксис"; done
if grep -qU $'\r' scripts/*.sh tools/*.sh VERSION 2>/dev/null; then
die "в файлах CRLF — сервер сверяет SHA256, сумма не сойдётся. Ничего не публикую"
fi
ok "швы на месте, синтаксис и переводы строк в порядке"
# ---------------------------------------------------------------- публикация
if git diff --quiet origin/"$BRANCH" -- VERSION scripts tools .gitea 2>/dev/null && [[ $new_version == "$ours" ]]; then
say "Содержимое не изменилось — публиковать нечего"
exit 0
fi
say "Публикую в $BRANCH"
git add -A
git -c user.name="fork-sync-bot" -c user.email="fork-sync@rukavishnikoff.ru" \
commit -q -m "Синхронизация с автором ${author_version}: швы и версия ${new_version}" \
-m "Автоматика CT 114. Проверки: швы на месте, bash -n, LF. Релиз и подпись — вручную с рабочей станции." \
|| die "не смог закоммитить"
curl -fsS -o /dev/null -H "Authorization: token ${TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/branches/${BRANCH}" || die "Gitea недоступна перед отправкой"
# Перенос идёт rebase'ом: наша ветка переписывается на свежий код автора, поэтому отправляем
# с --force-with-lease — если за это время в ветку что-то попало с рабочей станции, отправка
# откажется, а не затрёт чужой коммит.
git push -q --force-with-lease=refs/heads/"$BRANCH" origin "$BRANCH" \
|| die "не смог отправить ветку $BRANCH (возможно, в ней есть ваши свежие коммиты — тогда синхронизируйте вручную)"
ok "ветка $BRANCH обновлена: $(git --no-pager log --oneline -1)"
cat <<EOF
Готово. Дальше вручную, с рабочей станции:
1) git fetch origin && git checkout $BRANCH && git reset --hard origin/$BRANCH
2) описать изменения в FORK-CHANGELOG.md (раздел уже добавлен)
3) bash tools/release.sh ~/.ssh/3x-ui-kit-fork/release_key — подпись и релиз
4) git tag v$new_version && git push origin $BRANCH v$new_version
EOF
+589 -116
View File
@@ -25,6 +25,9 @@ import sys
GITEA = os.environ.get("FORK_GITEA_BASE", "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS") GITEA = os.environ.get("FORK_GITEA_BASE", "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS")
SIGNER_FILE = os.environ.get("FORK_SIGNER_PUB", "tools/fork-signer.pub") SIGNER_FILE = os.environ.get("FORK_SIGNER_PUB", "tools/fork-signer.pub")
# README.md форка — НАШ файл целиком: авторское описание из него убрано, чтобы никто не
# ставил сервер авторской командой и не оказался на авторском канале обновлений. Шва здесь нет:
# при обновлении из авторского проекта README конфликтует и решается в нашу пользу (см. FORK.md).
HOOK = """ HOOK = """
# --- 3X-UI_KIT (форк hacker07): локальные команды и хуки --- # --- 3X-UI_KIT (форк hacker07): локальные команды и хуки ---
# Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются. # Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются.
@@ -57,7 +60,7 @@ def main():
args.remove("--check") args.remove("--check")
root = pathlib.Path(args[0] if args else ".").resolve() root = pathlib.Path(args[0] if args else ".").resolve()
if not (root / "VERSION").exists(): if not (root / "VERSION").exists():
die("%s не похож на репозиторий 3X-UI KIT (нет VERSION)" % root) die("%s не похож на репозиторий 3X-UI_autoVPS (нет VERSION)" % root)
os.chdir(root) os.chdir(root)
version = (root / "VERSION").read_text(encoding="utf-8").strip() version = (root / "VERSION").read_text(encoding="utf-8").strip()
@@ -83,124 +86,544 @@ 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',
' echo "${B}kit fix${N} – безопасные исправления (службы, права, автообновление, сертификат, время)"\n echo\n',
False,
' # Наш шов: строку-описание команды не печатаем — при установке это лишний шум,\n # а смысл виден по результату (подробности — в kit help).\n',
'авто-шов: replace, строк 2',
),
(
'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',
' ask_num "Выбор [0-10]: "; c=$REPLY\n',
False,
' ask_num "Выбор [0-11]: "; c=$REPLY\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"; sha "${FILES[@]}"; } >SHA256SUMS',
False,
"{ echo \"# 3X-UI KIT $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" \'',
"«что нового» ищет и в наших записях",
), ),
] ]
@@ -236,6 +659,56 @@ def main():
p.write_text(text, encoding="utf-8", newline="") p.write_text(text, encoding="utf-8", newline="")
print("✓ %-20s %-56s (%s)" % (path, short, why)) print("✓ %-20s %-56s (%s)" % (path, short, why))
# Массовая замена бренда в файлах автора: строки, которые видит человек (справка, версия,
# описания служб, баннер). Строку проверки шапки SHA256SUMS не трогаем — её правит отдельный шов,
# а сама шапка ещё один релиз пишется в старом виде (иначе уже установленные серверы откажутся
# принимать обновление: они сверяют точный вид строки).
globals_ = [
("scripts/kit.sh", "3X-UI KIT", "3X-UI_autoVPS"),
("scripts/3x-ui.sh", "3X-UI KIT", "3X-UI_autoVPS"),
("scripts/hysteria2.sh", "3X-UI KIT", "3X-UI_autoVPS"),
("scripts/kit-sub.py", "3X-UI KIT", "3X-UI_autoVPS"),
]
for path, old, new in globals_:
p = pathlib.Path(path)
if not p.exists():
print("✗ %s: файла нет" % path)
bad += 1
continue
text = p.read_text(encoding="utf-8", newline="")
# строки, где старое имя — часть служебной сверки, не переименовываем
keep = lambda line: "grep -qx" in line
if check:
if new not in text:
print("✗ %-20s нет нашего имени «%s»" % (path, new))
bad += 1
continue
left = [l for l in text.splitlines() if old in l and not keep(l)]
if left:
print("✗ %-20s осталось старое имя в %d строк(ах)" % (path, len(left)))
bad += 1
continue
print("✓ %-20s бренд: %s" % (path, new))
continue
out = []
changed = 0
for line in text.splitlines(keepends=True):
if old in line and not keep(line):
out.append(line.replace(old, new))
changed += line.count(old)
else:
out.append(line)
if not changed:
if new in text:
print("– %-20s уже переименовано" % path)
continue
print("✗ %-20s не нашёл «%s»" % (path, old))
bad += 1
continue
p.write_text("".join(out), encoding="utf-8", newline="")
print("✓ %-20s переименовано вхождений: %d" % (path, changed))
if bad: if bad:
if check: if check:
die("якорей не найдено: %d — автор изменил код, пересмотри правки в tools/fork_apply.py" % bad) die("якорей не найдено: %d — автор изменил код, пересмотри правки в tools/fork_apply.py" % bad)
+59 -9
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Подпись релиза 3X-UI KIT. Запускает только автор, на своём компьютере, перед тегом: # Подпись релиза 3X-UI_autoVPS. Запускает только автор, на своём компьютере, перед тегом:
# #
# tools/release.sh [~/.ssh/3xui_kit_release] # tools/release.sh [~/.ssh/3xui_kit_release]
# #
# Проверяет, что версии сходятся и установщик 3X-UI не подменили, пишет SHA256SUMS, # Проверяет, что версии сходятся и установщик 3X-UI не подменили, пишет SHA256SUMS,
# подписывает его закрытым ключом (ssh-keygen спросит пароль ключа) и сразу проверяет # подписывает его закрытым ключом (ssh-keygen спросит пароль ключа) и сразу проверяет
# подпись открытым ключом, записанным в kit. Сервер 3X-UI KIT ставит обновление, только # подпись открытым ключом, записанным в kit. Сервер 3X-UI_autoVPS ставит обновление, только
# если эта проверка у него тоже сошлась. Закрытый ключ никуда не отправляется. # если эта проверка у него тоже сошлась. Закрытый ключ никуда не отправляется.
set -euo pipefail set -euo pipefail
@@ -13,10 +13,11 @@ 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}
[[ -f $key ]] || die "Нет закрытого ключа $key. Создать: ssh-keygen -t ed25519 -C \"3X-UI KIT releases\" -f $key" [[ -f $key ]] || die "Нет закрытого ключа $key. Создать: ssh-keygen -t ed25519 -C \"3X-UI_autoVPS releases\" -f $key"
NS="3x-ui-kit-release" NS="3x-ui-kit-release"
ID="releases@3x-ui-kit" ID="releases@3x-ui-kit"
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) 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)
@@ -29,12 +30,55 @@ done
(grep -q "^## v$v" CHANGELOG.md || grep -q "^## v$v" FORK-CHANGELOG.md) || die "Нет раздела ## v$v ни в CHANGELOG.md, ни в FORK-CHANGELOG.md (форк ведёт свои записи в FORK-CHANGELOG.md)" (grep -q "^## v$v" CHANGELOG.md || grep -q "^## v$v" FORK-CHANGELOG.md) || die "Нет раздела ## v$v ни в CHANGELOG.md, ни в FORK-CHANGELOG.md (форк ведёт свои записи в FORK-CHANGELOG.md)"
ok "Версия $v во всех скриптах и в CHANGELOG" ok "Версия $v во всех скриптах и в CHANGELOG"
# Переводы строк. Сервер получает файл из git (там LF), а суммы считаются по рабочей копии:
# если в рабочей копии окажутся CR, суммы не сойдутся и kit update откажется ставить релиз.
for f in "${FILES[@]}"; do
[[ -z $(tr -cd "\r" <"$f") ]] || die "$f: переводы строк Windows (CR). Уберите: tr -d '\r' < \"$f\" > \"$f.lf\" && mv \"$f.lf\" \"$f\""
done
ok "Переводы строк во всех файлах релиза — LF"
export PY
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 "Все наши функции определены раньше своих вызовов"
# python3 есть не везде (на Windows обычно только python) — берём тот, что есть.
py=$(command -v python3 || command -v python || true)
[[ -n $py ]] || die "нет python — им проверяются меню (tools/check-menus.py)"
"$py" tools/check-menus.py >/dev/null || die "подсказка «Выбор [0-N]» не совпадает с числом пунктов меню (tools/check-menus.py)"
ok "Подсказки меню совпадают с числом пунктов"
[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит." [[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит."
# Часть проверок сверяется с 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"
@@ -42,7 +86,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 совпадают с официальными"
@@ -55,11 +100,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 совпадают с установщиком и официальными суммами"
@@ -70,9 +117,12 @@ pub=$(ssh-keygen -y -f "$key" | awk '{print $1, $2}')
signers scripts/kit.sh | awk '{print $1, $2}' | grep -qxF "$pub" || die "Ключа $key нет в KIT_SIGNERS – серверы не примут такую подпись." signers scripts/kit.sh | awk '{print $1, $2}' | grep -qxF "$pub" || die "Ключа $key нет в KIT_SIGNERS – серверы не примут такую подпись."
ok "Ключ подписи записан в kit" ok "Ключ подписи записан в kit"
{ echo "# 3X-UI KIT $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"
+2 -2
View File
@@ -113,7 +113,7 @@ class RestoreEnv(unittest.TestCase):
def bad(self, over): def bad(self, over):
r = restore_env(over) r = restore_env(over)
self.assertNotIn("ENV-OK", r.stdout) self.assertNotIn("ENV-OK", r.stdout)
self.assertIn("Это не резервная копия 3X-UI KIT", r.stdout + r.stderr) self.assertIn("Это не резервная копия 3X-UI_autoVPS", r.stdout + r.stderr)
def test_mtproto_inner_sed(self): def test_mtproto_inner_sed(self):
for v in (b"MTPROTO_INNER='[0-9]*.*/touch \\/tmp\\/pwn-test/e;#'", b"MTPROTO_INNER='1/x/;1e touch /tmp/pwn-test #'", b"MTPROTO_INNER=1/x/e"): for v in (b"MTPROTO_INNER='[0-9]*.*/touch \\/tmp\\/pwn-test/e;#'", b"MTPROTO_INNER='1/x/;1e touch /tmp/pwn-test #'", b"MTPROTO_INNER=1/x/e"):
@@ -148,7 +148,7 @@ class RestoreEnv(unittest.TestCase):
self.assertIn("байты " + name, check(p).stderr) self.assertIn("байты " + name, check(p).stderr)
def test_3xui_txt(self): def test_3xui_txt(self):
good = "3X-UI KIT – данные для входа\n\nПанель: https://1.2.3.4/x/\n\tvless://a@1.2.3.4:443?x=1#Мой 🙂\n".encode() good = "3X-UI_autoVPS – данные для входа\n\nПанель: https://1.2.3.4/x/\n\tvless://a@1.2.3.4:443?x=1#Мой 🙂\n".encode()
r = check(archive([(k, v) for k, v in ENVS.items()] + [("root/3x-ui.txt", good)])) r = check(archive([(k, v) for k, v in ENVS.items()] + [("root/3x-ui.txt", good)]))
self.assertEqual(r.returncode, 0, r.stderr) self.assertEqual(r.returncode, 0, r.stderr)
for bad in (b"\0", b"\x1b]0;x\x07", b"\x1b[2J", b"\r", b"\xff", b"\xc2\x9b", b"\x7f"): for bad in (b"\0", b"\x1b]0;x\x07", b"\x1b[2J", b"\r", b"\xff", b"\xc2\x9b", b"\x7f"):
+2 -2
View File
@@ -102,7 +102,7 @@ class SafeEnv(unittest.TestCase):
def rejected(self, name, text): def rejected(self, name, text):
code, out = run(name, text, 'echo "RESULT=$RESULT"\n') code, out = run(name, text, 'echo "RESULT=$RESULT"\n')
self.assertNotEqual(code, 0, out) self.assertNotEqual(code, 0, out)
self.assertIn("Это не резервная копия 3X-UI KIT", out) self.assertIn("Это не резервная копия 3X-UI_autoVPS", out)
self.assertNotIn("\nOK\n", "\n" + out) self.assertNotIn("\nOK\n", "\n" + out)
def test_result_in_backup_env(self): def test_result_in_backup_env(self):
@@ -148,7 +148,7 @@ class SafeEnv(unittest.TestCase):
r = subprocess.run(["bash", "-c", script, "t", p], capture_output=True) r = subprocess.run(["bash", "-c", script, "t", p], capture_output=True)
out = (r.stdout + r.stderr).decode("utf-8", "replace") out = (r.stdout + r.stderr).decode("utf-8", "replace")
self.assertNotEqual(r.returncode, 0, "%s %r: %s" % (name, data, out)) self.assertNotEqual(r.returncode, 0, "%s %r: %s" % (name, data, out))
self.assertIn("Это не резервная копия 3X-UI KIT", out, "%s %r" % (name, data)) self.assertIn("Это не резервная копия 3X-UI_autoVPS", out, "%s %r" % (name, data))
self.assertNotIn("\nOK\n", "\n" + out) self.assertNotIn("\nOK\n", "\n" + out)
def test_substitutions(self): def test_substitutions(self):
+1 -1
View File
@@ -128,7 +128,7 @@ class Rules(unittest.TestCase):
link = ks.routing_payload("happ", loaded) link = ks.routing_payload("happ", loaded)
self.assertTrue(link.startswith("happ://routing/onadd/")) self.assertTrue(link.startswith("happ://routing/onadd/"))
h = json.loads(base64.b64decode(link.split("/onadd/")[1])) h = json.loads(base64.b64decode(link.split("/onadd/")[1]))
self.assertEqual(h["Name"], "3X-UI KIT") self.assertEqual(h["Name"], "3X-UI_autoVPS")
self.assertEqual(h["GlobalProxy"], "false") self.assertEqual(h["GlobalProxy"], "false")
self.assertEqual(h["ProxySites"], ["geosite:youtube", "domain:example.org", "full:api.example.com"]) self.assertEqual(h["ProxySites"], ["geosite:youtube", "domain:example.org", "full:api.example.com"])
self.assertEqual(h["ProxyIp"], ["geoip:telegram", "203.0.113.0/24"]) self.assertEqual(h["ProxyIp"], ["geoip:telegram", "203.0.113.0/24"])