Compare commits

...
21 Commits
Author SHA1 Message Date
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
hacker07 b61a3225d8 Релиз v1.2.5: подпись ключом форка
CI / check (push) Canceled after 0s
2026-10-12 19:46:46 +03:00
hacker07 70e2a499c7 v1.2.5: суммы релиза в POSIX-виде (без звёздочки) — kit сверяет имя строго 2026-10-12 19:40:04 +03:00
23 changed files with 1604 additions and 483 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
+68 -2
View File
@@ -3,6 +3,72 @@
Здесь записи о наших версиях. Файл автора (`CHANGELOG.md`) мы не трогаем — так обновления
автора не конфликтуют с нашими записями.
## 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.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.5: суммы релиза в формате, который понимает kit
* Исправлена ошибка нашего релизного конвейера: на Windows `sha256sum` пишет суммы как
`hash *файл`, а `kit` на сервере сверяет имя строго (`$2 == "файл"`) — то есть `kit update`
отклонил бы релизы v1.2.1–v1.2.4 как «в подписанном списке нет такого файла». Теперь
`tools/release.sh` приводит список к POSIX-виду (`hash файл`) **до** подписи.
* `scripts/3x-ui-local.sh` принимает оба формата, чтобы не спотыкаться о суммы, собранные на
другой машине.
* Проверка теперь повторяет логику самого `kit`: подпись `SHA256SUMS` + строгая сверка имени
каждого файла из подписанного списка (а не только `sha256sum -c`, который звёздочку прощает).
## v1.2.4: проверка релиза отдельной командой
* Флаг `--verify-only`: скачать релиз, проверить подпись `SHA256SUMS` и SHA256 установщика и выйти —
@@ -42,7 +108,7 @@
* Ping отключается правилом ufw (`before.rules`), а не общесистемным `icmp_echo_ignore_all`:
видно в `ufw status`, отменяется одной командой и не глушит ICMP для внутренних интерфейсов.
## v1.2.1: сборка форка поверх 3X-UI KIT 1.2
## v1.2.1: сборка форка поверх 3X-UI_autoVPS 1.2
* Источник файлов и обновлений — свой репозиторий Gitea: установка и `kit update` берут релиз
оттуда, а подпись релиза проверяется ключом форка. Авторский ночной апдейт приходит в наш
@@ -54,4 +120,4 @@
* Швы в коде автора описаны в `tools/fork_apply.py` (источник правды), синхронизация с автором и
проверки — `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`.
+181 -75
View File
@@ -1,108 +1,214 @@
# Форк 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/3x-ui-local.sh` | только мы |
| Описание «швов» в коде автора | `tools/fork_apply.py` | только мы |
| Синхронизация с автором и проверки | `tools/fork-sync.sh` | только мы |
| Записи о наших версиях | `FORK-CHANGELOG.md` | только мы |
| `README.md` | целиком, включая картинки в `manuals/assets/` | только мы |
| Файлы автора (`scripts/kit.sh`, `scripts/3x-ui.sh`, …) | как есть | только автор |
Ключевая идея: **в файлах автора мы правим только «швы»** — несколько констант и одну строку
подключения. Всё, что мы дописываем по существу, лежит в нашем файле, которого автор не знает.
Ключевая идея: в файлах автора мы правим только «швы» — несколько констант, одну строку подключения
и нормализацию сумм в релизном скрипте. Всё, что мы дописываем по существу, лежит в наших файлах.
Швы описаны в одном месте (`tools/fork_apply.py`) и накатываются только на чистое дерево автора;
если автор изменил нужную строку — инструмент останавливается и говорит об этом, а не делает
полу-патч.
## Ветки
## Ветки и релизы
- `upstream` — точная копия ветки автора (`main`), только вперёд.
- `dev` — наша сборка: `upstream` + швы + наш файл. Из неё делаются релизы.
- релизы — теги `v<номера>`; номер = `<версия автора>.<наш уровень>` (например, автор 1.2 → наш
первый релиз 1.2.1, следующий наш — 1.2.2, после релиза автора 1.3 — 1.3.1).
- `dev` — наша сборка: `upstream` + швы + наши файлы; из неё делаются релизы.
- `main` — то же состояние плюс документация после релиза (по нему считается «последняя версия»).
- релизы — теги `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
cd H:/My_Proj/Fork_3X-UI_KIT
bash tools/fork-sync.sh # подтянуть автора, перенести наши правки, прогнать проверки
# при конфликте — разобрать его вручную, инструмент сам ничего не публикует
# при конфликте — разобрать вручную: инструмент сам ничего не публикует
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)
git add -A && git commit -m "Релиз v$(cat VERSION): …" && git tag v$(cat VERSION)
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`).
- Закрытая часть — только на рабочей станции владельца (`~/.ssh/3x-ui-kit-fork/release_key`),
с паролем; в репозиторий, CI и на серверы не попадает. Кто получит ключ — получит root на всех
серверах, которые обновляются из этого репозитория.
- Смена ключа: подпиши новый релиз старым ключом, иначе серверы его не примут.
## Наши команды
```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` — покажет план и ничего не изменит.
- Открытая часть ключа — `tools/fork-signer.pub` и в `KIT_SIGNERS` (в `scripts/kit.sh`); она же
вшита в `scripts/3x-ui-local.sh`, чтобы проверять подпись до запуска установщика.
- Закрытая часть — только на рабочей станции владельца (`~/.ssh/3x-ui-kit-fork/release_key`).
В репозиторий, CI и на серверы не попадает: кто получит ключ — получит root на всех серверах,
которые обновляются из этого репозитория. Пароль на ключ пока не установлен
(`ssh-keygen -p -f ~/.ssh/3x-ui-kit-fork/release_key`).
- Смена ключа: новый релиз подписывается **старым** ключом, иначе серверы его не примут.
## Грабли, о которых стоит помнить
1. **Ночное автообновление** (`kit-update.timer`, 03:00) перезаписывает `/usr/local/bin/kit`
подписанным релизом из настроенного репозитория. Поэтому канал обновления и швы — один шаг:
сервер должен смотреть в этот репозиторий, иначе наши правки исчезнут ночью.
2. **Переводы строк.** Релиз подписывается по контрольным суммам файлов в рабочей копии, а сервер
сверяет их с файлами из Gitea. При `core.autocrlf=true` суммы не сойдутся (CRLF против LF):
в этом клоне должно быть `git config core.autocrlf false`.
3. **Наш файл обязан быть в списке релиза** (`tools/release.sh`, `FILES=(…)`) и в списке загрузки
`fetch_release()` — иначе при обновлении `kit.sh` приедет новый, а `kit-local.sh` останется старым.
4. **Панель 3X-UI ставится с GitHub** по закреплённой версии и SHA256 — это шаг автора, форк его не
трогает. Для установки с нуля серверу нужен доступ к GitHub.
1. **Ночное автообновление** (`kit-update.timer`, 03:00) перезаписывает `/usr/local/bin/kit` тем, что
лежит в настроенном репозитории. Канал и наши правки — один шаг, а не два.
2. **Переводы строк.** Релиз подписывается по суммам рабочей копии, а сервер сверяет их с файлами из
Gitea: при `core.autocrlf=true` суммы не сойдутся (CRLF против LF). В клоне должно быть
`git config core.autocrlf false` (и `.gitattributes`), проверка есть в `tools/fork-sync.sh`.
3. **Наш файл обязан быть в двух списках** — `FILES=(…)` в `tools/release.sh` и список загрузки
`fetch_release()` в `scripts/kit.sh`, иначе после обновления `kit-local.sh` останется старым.
4. **Панель 3X-UI ставится с GitHub** по закреплённой версии и SHA256 — это шаг автора; для установки
с нуля серверу нужен доступ к 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`.
+168 -283
View File
@@ -1,311 +1,196 @@
<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](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)
Сборка со **своим каналом обновлений**: сервер ставится и обновляется из этого репозитория, релизы
подписаны, ночное автообновление не вернёт чужую версию. В основе — открытый проект 3X-UI_autoVPS.
</div>
---
## Что ставится
> [!WARNING]
> **Проект – набор скриптов с открытым кодом.** Он не оказывает услуг связи и не предоставляет доступ
> к серверам. Вы ставите его на собственный сервер и сами отвечаете за соблюдение законодательства
> своей страны. Проект создан в образовательных целях.
> [!TIP]
> **Вышла версия 1.2:** панель 3X-UI v3.9.0, меню `kit`, раздельная маршрутизация и `--restore`.
> [Что нового](https://github.com/itsnotkubrick/3X-UI_KIT/releases/tag/v1.2) ·
> сервер на 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)** и рекомендую его
(реферальная ссылка).
| Что | Зачем |
|---|---|
| Панель **3X-UI** + ядро **Xray** | управление подключениями и пользователями |
| **nginx** на порту 443 | разводит подключения по именам и путям, отдаёт сайт-заглушку |
| VPN-подключения | REALITY, Hysteria2, AmneziaWG, XHTTP, VLESS-WS, Trojan-gRPC, VMess-WS, Shadowsocks, TUIC, MTProto, WireGuard — какие выберете |
| **Подписка** (`kit-sub`) | одна ссылка на все устройства: Happ, Hiddify, v2rayN, Clash/Mihomo |
| **Сайт-заглушка** | на 443 и в ответе Hysteria2 — правдоподобный ИТ-сайт из нашего набора |
| **ufw**, обновления безопасности, синхронизация времени | защита сервера и корректная работа REALITY |
## Установка
Подключитесь к серверу по SSH и выполните одну команду на чистом VPS.
Панель 3X-UI, все протоколы на порту 443 и одна подписка на всё:
От root, на чистом сервере (Debian 11+ / Ubuntu 22.04+, 64-бит):
```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-кодом.
Подробно – подключение приложений, дополнительные пользователи и параметры – в **[инструкции](manuals/3x-ui.md)**.
Без флагов скрипт **спрашивает** всё по ходу: какие протоколы ставить (Enter — REALITY + AmneziaWG-3.1),
какую маскировку и домен, закрывать ли ответ на ping, включать ли автообновления безопасности,
менять ли порт SSH (Enter — оставить 22), менять ли пароль root.
## Команды KIT
<img alt="Установка одной командой" src="manuals/assets/script-3x-ui-local.svg" width="860">
После установки на сервере появляется команда `kit`. Запустите её **без аргументов** – откроется меню, как у `x-ui`: выберите цифру, остальное спросит само. Те же действия одной строкой – в группах ниже (нажмите, чтобы раскрыть). Подробности – в [инструкции](manuals/3x-ui.md).
<details>
<summary><b>Пользователи</b> – <code>kit user</code></summary>
Если вопросы не нужны — те же ответы задаются флагами:
```bash
kit user add имя [--gb 50] [--days 30] [--devices 3] # добавить во все протоколы, показать подписку
kit user list # кто сколько израсходовал, до какого числа
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>
# минимум: свой домен и два протокола, без вопросов
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --protocols reality,awg3 --domain vpn.example.com
<details>
<summary><b>Сеть и маскировка</b> – <code>kit net</code></summary>
# всё, кроме WireGuard, и второй порт SSH
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --protocols все --ssh-port 2222
# ничего не спрашивать: ping закрыть, автообновления включить, порт и пароль не трогать
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --yes
```
Проверить канал, ничего не меняя:
```bash
kit net # протоколы, порты, сайт маскировки, отпечаток – одним экраном
kit net site [сайт.com] # сменить сайт маскировки (без аргумента – среди соседей по подсети)
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, панели и ядра
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --verify-only # подпись релиза и сумма установщика
bash <(curl -fsSL …/scripts/3x-ui-local.sh) --dry-run # что именно будет сделано
```
Копию из `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) |
| Зарубежной картой, Apple Pay, Google Pay | [Buy Me a Coffee](https://buymeacoffee.com/relo.cate) |
| USDT (TRC-20) | `TS83ViXrdezUpp1eFadqj1rBhGLZaba1c1` |
| TON | `UQBchO4XFPwF9MMa_tjXpwqTo8IL2FhUDyllhYuFo8WM-Qbf` |
| Ethereum (ERC-20) | `0xC06F6B3A029d7Ea00705B7028490744e2BC16799` |
| `--protocols all\|minimal\|список` | какие протоколы ставить: `all`, `minimal` (только REALITY) или через запятую. Невыбранные не создаются вообще |
| `--domain ваш.домен` | свой домен для маскировки (A-запись на сервер, свободный порт 80 — сертификат Let's Encrypt выпустит установщик) |
| `--sni сайт.ком` | маскировка под чужой сайт вместо своего домена |
| `--panel-on ip\|domain` | где открывать панель и подписку |
| `--site N\|random\|off` | сайт-заглушка: конкретный из нашего набора, случайный (по умолчанию) или не трогать |
| `--ssh-port N` | перевести SSH на порт N (прежний закроется только после вашей проверки) |
| `--harden` / `--no-harden` | закрыть ping и включить автообновления безопасности / не трогать это |
| `--yes` | не задавать вопросы (для автоматизации) |
| `--host IP`, `--user имя`, `--port N`, `--restore файл` | адрес в ссылках, имя первого клиента, порт REALITY, восстановление из копии `kit backup` |
| `--no-time`, `--no-check`, `--verify-only`, `--dry-run`, `-h` | служебные |
## Благодарности
## Что происходит по шагам
3X-UI KIT построен на работе авторов этих проектов:
[3X-UI](https://github.com/MHSanaei/3x-ui) ·
[Xray-core](https://github.com/XTLS/Xray-core) ·
[Mihomo](https://github.com/MetaCubeX/mihomo) ·
[Hysteria](https://github.com/apernet/hysteria) ·
[AmneziaWG](https://github.com/amnezia-vpn) ·
[XKeen](https://github.com/jameszeroX/XKeen)
1. **Проверка сервера**: root, Debian/Ubuntu, память, место на диске, занятые порты 80/443.
2. **Проверка релиза**: файлы берутся из этого репозитория по тегу, проверяется **подпись `SHA256SUMS`**
нашим ключом и SHA256 самого установщика. Не сошлось — ничего не запускается.
3. **Выбор протоколов** и запуск установщика: панель 3X-UI, выбранные подключения, nginx на 443,
сертификат, сайт-заглушка, служба подписки, правила ufw.
4. **Сайт-заглушка**: ставится случайный правдоподобный ИТ-сайт из нашего набора (10 штук) и
синхронизируется в ответ Hysteria2.
5. **Наши вопросы и правки**: закрыть ответ на ping, включить обновления безопасности, сменить порт
SSH, сменить пароль root.
6. **`kit fix`** — синхронизация времени и безопасные починки.
7. **Проверка**: `kit check` (порты, службы, подписка, сертификат, маскировка) и сводка
`kit local hardening`. В конце — как забрать ссылки и подписку.
## Протоколы
Один порт 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`.
## Сайт-заглушка
На 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 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.4
567e5326890669659bfab769a5ddb2a2863eb51493a975e16c84df6973ec5170 *scripts/3x-ui.sh
62fc23d886f91bb59ba47c4fb932d8fe49680271476c94e61238a0dde3002cd9 *scripts/hysteria2.sh
f63388be2e61a4fc546b78b3d8c640179f7ce41ac30bf4a329efb1b3e97faa5b *scripts/kit.sh
eac3e47491ce86487f05160952ff5b4ad8a3821954f082eee3516e40575fb574 *scripts/kit-sub.py
30579dfae24239d1c797dc8efac5b2b49116bc05058923df184b382f33fe265b *scripts/kit-local.sh
e97d3ac23f97d95fcc3d04b5e6751b28a4425ccb1bcb6c5a252bb9a9e52b1ade *scripts/3x-ui-local.sh
563a0b4eec3c77bbc34ac630d4fa5214f5c6726ae01468404fd704473110084e *VERSION
# 3X-UI KIT 1.2.7
# 3X-UI_autoVPS 1.2.7
6076d7484223bfa00189f13266b4a131e940a77ebc05ae4bf0373f7245e81c55 scripts/3x-ui.sh
2865f428bfd05e0310ca0cbdd41808c2e78c5daf34a3547d607711b248eb596c scripts/hysteria2.sh
5cceb4d526a883424c445c79b70cf9122d0ee87ef7f625798c197d4d5a93b777 scripts/kit.sh
4027ea2c6dbc9fd176356acac3f676445419c2f021146ed25dfe5a304c3ac5fb scripts/kit-sub.py
9bb4284d60d79f6e9b7674c53e05553c7ee788952398bde95a240fabec2e8a8f scripts/kit-local.sh
fa6708e34d6208e45b95b7247808d440d4c20520dbe2ff23f5df280891d555d9 scripts/3x-ui-local.sh
d9e8e8429c6a7d0b965e4fac26a5efff7103f055faa98cceb5bd4731fd24603a VERSION
+2 -2
View File
@@ -1,6 +1,6 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgozRJ+f9RB6qO9S9j3BWCDtC0bV
FHakRSSqtQPljLoQUAAAARM3gtdWkta2l0LXJlbGVhc2UAAAAAAAAABnNoYTUxMgAAAFMA
AAALc3NoLWVkMjU1MTkAAABA3dMDUsb5LWkr0YJ5X/rCfLxNTuwzHymGpDwOd+XIRWsnYY
eA60VFXtz5uCxAdiyKXXgg7Prs6nCq819EYCU2Ag==
AAALc3NoLWVkMjU1MTkAAABA5fMLiArp8/6KqJMfwZpwlBu6zf9YkXaayCinxpSb1U2gCx
bib/uVxP3BCGQlVIKHXbsZKSFkBHc+uYcTy6XkDA==
-----END SSH SIGNATURE-----
+1 -1
View File
@@ -1 +1 @@
1.2.4
1.2.7
+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

+283 -49
View File
@@ -1,26 +1,40 @@
#!/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 установщика;
# 3) запускает установщик автора (scripts/3x-ui.sh) — панель 3X-UI, протоколы, сайт-заглушка,
# маскировка, сертификат, подписка, ufw; все обычные флаги установщика можно передавать как есть;
# 4) досыпает то, чего у автора нет (поведение из StressOzz/vps-setup, но безопаснее):
# ответ на ping закрывается правилом ufw, включаются обновления безопасности;
# 5) при желании добавляет второй порт SSH (--ssh-port 2222) — прежний остаётся рабочим;
# 6) в конце: kit fix (время и безопасные починки), kit check, kit local hardening.
# маскировка, сертификат, подписка, ufw. Флаги установщика можно передавать как есть,
# а можно ничего не передавать — тогда он задаст вопросы сам (в том числе про домен);
# 4) спрашивает, какие протоколы ставить: невыбранные не создаются вообще — ни подключения в панели,
# ни записей в nginx, ни ссылок в подписке (за это отвечает флаг автора --protocols);
# 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
# ... 3x-ui-local.sh --ssh-port 2222 --no-harden
# Запуск:
# # одна команда, дальше — вопросы:
# bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui-local.sh)
# # или сразу с ответами, без вопросов:
# ... 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 ФАЙЛ
# --harden закрыть ping и включить автообновления, без вопросов
# --no-harden не трогать ping и автообновления
# --ssh-port N добавить порт SSH N (прежний порт продолжает работать)
# --no-time не запускать kit fix (синхронизация времени и безопасные починки)
# --yes ни о чём не спрашивать (ping закрою, автообновления включу, порт и пароль — нет)
# --no-time не запускать kit fix
# --no-check не запускать в конце kit check
# --verify-only только проверить релиз (подпись и SHA256) и выйти
# --dry-run показать план и ничего не делать
# -h | --help эта справка
#
@@ -28,12 +42,12 @@
set -Eeuo pipefail
KIT_VERSION="1.2.4" # версия сборки форка; должна совпадать с VERSION релиза
KIT_VERSION="1.2.7" # версия сборки форка; должна совпадать с VERSION релиза
GITEA="${KIT_FORK_BASE:-https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS}"
NS="3x-ui-kit-release"
SIG_ID="releases@3x-ui-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'
[[ -t 1 ]] || { G=; Y=; R=; B=; D=; N=; }
@@ -43,40 +57,52 @@ die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; }
usage() {
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)
--sni сайт.com маскировка под чужой сайт
--panel-on domain | ip где открывать панель и подписку
--host IP, --user имя, --port N, --protocols …, --restore файл
Флаги этого скрипта:
--no-harden не отключать ответ на ping и не включать автообновления безопасности
--ssh-port N добавить порт SSH N (прежний порт продолжает работать)
--harden закрыть ping и включить автообновления, без вопросов
--no-harden не трогать ping и автообновления
--ssh-port N перевести SSH на порт N (прежний закроется после проверки входа)
--yes ни о чём не спрашивать (для автоматизации)
--site N|random|off сайт-заглушка: конкретный из нашего набора, случайный (по умолчанию) или
не трогать авторскую страницу (то же: --no-site)
--no-sub не показывать в конце подписку, QR-код и ссылки по протоколам
--no-time не запускать kit fix (время и безопасные починки)
--no-check не запускать в конце kit check
--verify-only только проверить релиз (подпись и SHA256) и выйти
--dry-run показать план и ничего не делать
-h | --help эта справка
Что скрипт делает: проверяет сервер → скачивает релиз своей же версии из нашего Gitea,
проверяет подпись SHA256SUMS и SHA256 установщика → запускает установщик автора → досыпает
защиту (ping off правилом ufw, автообновления безопасности, при желании второй порт SSH) →
kit fix и kit check.
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-код и ссылки
while (($#)); do
case $1 in
--harden) harden=yes ;;
--no-harden) harden=no ;;
--ssh-port) ssh_port=${2:-}; shift || die "--ssh-port: нужен номер порта" ;;
--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 ;;
--no-time) fix_time=no ;;
--no-check) do_check=no ;;
--verify-only) verify_only=yes ;;
@@ -86,20 +112,48 @@ while (($#)); do
esac
shift
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' "$*"; }
ASK_SRC=${KIT_ASK_SRC:-/dev/tty} # откуда читать ответы (в тестах подменяется файлом)
interactive() { [[ $assume_yes == no && -r $ASK_SRC ]]; }
if [[ $dry == yes ]]; then
say "план установки (--dry-run: ничего не делаю)"
pre "версия сборки: $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
pre "маскировка: своя заглушка/домен — как решит установщик автора"
if [[ $harden == yes ]]; then pre "защита: ping off (правилом ufw) + автообновления безопасности"; else pre "защита: пропускаю (--no-harden)"; 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
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"
[[ $show_sub == yes ]] && pre "в самом конце: подписка, QR-код и ссылки по протоколам (--no-sub — не надо)"
echo
say "запуск без --dry-run выполнит всё это на сервере"
exit 0
@@ -121,7 +175,7 @@ done
printf '%s namespaces="%s" %s\n' "$SIG_ID" "$NS" "$SIGNER_PUB" >"$tmp/allowed_signers"
ssh-keygen -Y verify -f "$tmp/allowed_signers" -I "$SIG_ID" -n "$NS" -s "$tmp/SHA256SUMS.sig" <"$tmp/SHA256SUMS" >/dev/null 2>&1 \
|| die "подпись релиза v$KIT_VERSION не сошлась с ключом форка — это не наш релиз, ничего не запускаю"
sum=$(awk '$2 == "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 нет установщика — релиз собран неверно"
got=$(sha256sum "$tmp/scripts/3x-ui.sh" | awk '{print $1}')
[[ $got == "$sum" ]] || die "SHA256 установщика не совпал с подписанным — не запускаю"
@@ -145,14 +199,98 @@ printf ' %s\n' "${PRETTY_NAME:-Linux}, ядро $(uname -r), памяти $(awk
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
# ------------------------------------------------------------------ какие протоколы ставить
# Список, порядок и набор по умолчанию — как у установщика автора (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[*]}")"
}
# ------------------------------------------------------------------ установка
if [[ -z $proto_flag ]]; then
protos=$(ask_protocols)
INSTALLER_ARGS+=(--protocols "$protos")
else
protos=$proto_flag
fi
say "протоколы: $protos — под остальные не создаётся ничего"
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,
# иначе установщик начнёт читать наш собственный скрипт вместо ответов.
if [[ -t 0 ]] || { : </dev/tty; } 2>/dev/null; then
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/tty || die "установщик остановился с ошибкой — он ничего не тронул дальше своего лога"
if [[ -r /dev/tty ]]; then
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/tty || die "установщик остановился с ошибкой — смотрите его вывод выше"
else
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/null || die "установщик остановился с ошибкой (нет терминала — вопросы задать некому)"
fi
@@ -160,26 +298,100 @@ command -v kit >/dev/null || die "после установки нет кома
# ------------------------------------------------------------------ наши дополнения
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|д|да)$ ]]
}
# Сайт-заглушка: у автора это случайные «кофейни и пекарни», у нас — правдоподобный ИТ-сайт.
# Меняем не файл вручную, а командой форка: она же обновляет копию страницы в ответе Hysteria2,
# иначе 443/tcp и UDP-порт начнут отвечать разными страницами (это видно снаружи).
case $site in
off) say "заглушку не трогаю (--no-site): остаётся страница автора" ;;
random)
say "ставлю сайт-заглушку: случайный ИТ-сайт из нашего набора"
kit local site || warn "не удалось поставить заглушку — позже: kit local site"
;;
*)
say "ставлю сайт-заглушку №$site"
kit local site "$site" || warn "не удалось поставить заглушку — позже: kit local site $site"
;;
esac
if [[ $fix_time == yes ]]; then
say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)"
kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод"
fi
if [[ $harden == yes ]]; then
say "защита сервера: закрываю ответ на ping и включаю обновления безопасности"
kit local ping off || warn "не удалось закрыть ping — повторите: kit local ping off"
kit local upgrades on || warn "не удалось включить автообновления — повторите: kit local upgrades on"
# ping и автообновления: --harden/--no-harden решают без вопросов, иначе спрашиваем
want_ping=$harden; want_update=$harden
[[ $want_ping == auto ]] && want_ping=yes
[[ $want_update == auto ]] && want_update=yes
if [[ $harden == auto ]] && interactive; then
if ask_yes "Закрыть ответ сервера на ping? Снаружи пинг перестанет отвечать (подписка и VPN не затрагиваются)." y; then want_ping=yes; else want_ping=no; fi
if ask_yes "Включить автообновления безопасности? Только security-обновления, без перезагрузки." y; then want_update=yes; else want_update=no; fi
fi
if [[ $want_ping == yes ]]; then
say "закрываю ответ на ping (правилом ufw)"
kit local ping off || warn "не удалось — повторите: kit local ping off"
fi
if [[ $want_update == yes ]]; then
say "включаю автообновления безопасности"
kit local upgrades on || warn "не удалось — повторите: kit local upgrades on"
fi
if [[ -n $ssh_port ]]; then
say "добавляю порт SSH $ssh_port (прежний порт продолжает работать)"
kit local ssh "$ssh_port" || warn "порт не добавился — сделайте это отдельно: kit local ssh $ssh_port"
cat <<EOF
# порт 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}"
}
${Y}Дальше обязательно:${N}
проверьте вход во ВТОРОМ окне: ssh -p $ssh_port root@<IP сервера>
убедились — закройте прежние порты: kit local ssh --close-old $ssh_port
EOF
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)"
}
[[ -n $ssh_port ]] || ssh_port=$(ask_ssh_port)
if [[ -n $ssh_port ]]; then
say "перевожу SSH на порт $ssh_port: новый добавляю, прежний пока остаётся рабочим"
kit local ssh "$ssh_port" || warn "порт не добавился — сделайте отдельно: kit local ssh $ssh_port"
fi
# пароль root: в флагах его нет намеренно — значение в командной строке видно в списке процессов
if interactive; then
if ask_yes "Сменить пароль root прямо сейчас?" n; then
kit local passwd || warn "не получилось — повторите: kit local passwd"
fi
fi
# ------------------------------------------------------------------ проверка
@@ -192,9 +404,31 @@ fi
cat <<EOF
${G}${B}Готово.${N} Сервер поставлен из нашей сборки (3X-UI KIT $KIT_VERSION, канал: наш Gitea).
${G}${B}Готово.${N} Сервер поставлен из нашей сборки (3X-UI_autoVPS $KIT_VERSION, канал: наш Gitea).
подписка и ссылки: kit user link admin (или страница подписки из вывода установщика)
если что-то не так: kit check --deep, затем kit fix
вернуться к авторскому каналу: положить авторский kit поверх и выполнить kit update --force
EOF
# ------------------------------------------------------------------ подписка, QR-код и доступы
# Показываем в самом конце, после всех проверок: это то, что человек уносит с собой.
# Печатает авторская команда (подписка + QR-код через qrencode + ссылки по протоколам),
# поэтому вывод не расходится с тем, что показывает `kit` потом.
if [[ $show_sub == yes ]]; then
sub_user=admin
for ((_i = 0; _i < ${#INSTALLER_ARGS[@]}; _i++)); do
[[ ${INSTALLER_ARGS[_i]} == --user ]] && sub_user=${INSTALLER_ARGS[_i + 1]:-admin}
done
say "подписка и доступы — это то, что стоит сохранить"
printf ' подписка — одна ссылка на все устройства; QR-код ниже наводите камерой в приложении\n'
printf ' приложения: Happ, Hiddify, Karing, v2rayN, Clash/Mihomo\n'
printf ' отдельные ссылки по протоколам — ниже же (если приложение не умеет подписку)\n'
printf ' %sпозже то же самое: kit user link %s --all (или меню: kit)%s\n' "$D" "$sub_user" "$N"
if ! kit user link "$sub_user" --all; then
warn "не смог показать ссылки для «$sub_user» — список пользователей: kit user list"
fi
else
say "ссылки и QR-код не показываю (--no-sub). Посмотреть: kit user link admin --all"
fi
+22 -22
View File
@@ -30,7 +30,7 @@ wait_apt_idle() {
XUI_VERSION="v3.9.0"
# SHA256 установщика 3X-UI этой версии: тег могут передвинуть, а хеш – нет (проверено 2026-09-30).
XUI_INSTALL_SHA256="18616fe26c8f6c92db6daa2dcd7cd53c5143ee69ecfd26d8ea6dc9b2c78607a6"
KIT_VERSION="1.2.4"
KIT_VERSION="1.2.7"
# kit и kit-sub берём из того же релиза, что и этот скрипт, а не из меняющейся ветки main.
KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$KIT_VERSION"
# Ядро Xray для панели. С 26.7.x клиенты на Mihomo и sing-box (Hiddify, FlClash,
@@ -372,15 +372,15 @@ kit_banner() {
echo
printf '%s' "$G"
cat <<'ART'
_ _ _ _ _ _ _
(_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __
| | __/ __| '_ \ / _ \| __| |/ / | | | '_ \| '__| |/ __| |/ /
| | |_\__ \ | | | (_) | |_| <| |_| | |_) | | | | (__| <
|_|\__|___/_| |_|\___/ \__|_|\_\\__,_|_.__/|_| |_|\___|_|\_\
_ _ _ ___ _____
| | | | __ _ ___| | _____ _ __ / _ \___ |
| |_| |/ _` |/ __| |/ / _ \ '__| | | | | / /
| _ | (_| | (__| < __/ | | |_| |/ /
|_| |_|\__,_|\___|_|\_\___|_| \___//_/
ART
printf '%s' "$N"
echo
echo "${B}3X-UI KIT $KIT_VERSION${N} · it's not Kubrick. it's just a VPN."
echo "${B}3X-UI_autoVPS $KIT_VERSION${N} · сборка форка: панель 3X-UI и VPN-протоколы на одном порту"
echo "${D}на основе панели 3X-UI (MHSanaei/3x-ui) и ядра Xray · github.com/itsnotkubrick/3X-UI_KIT${N}"
}
@@ -761,7 +761,7 @@ main() {
[[ $SINGLE == yes ]] && links=$(sed "s/^\(tg:\/\/proxy?\)\(.*\)port=${INNER[mtproto]}/\1\2port=443/" <<<"$links")
umask 077
{
echo "3X-UI KIT (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"
echo "3X-UI_autoVPS (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"
echo
echo "Панель: $panel_url"
echo "Логин: $XUI_USERNAME"
@@ -1480,7 +1480,7 @@ setup_nginx() {
local panel_path=/${XUI_WEB_BASE_PATH#/}
panel_path=${panel_path%/}/
{
echo "# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске."
echo "# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске."
echo "stream {"
echo " map \$ssl_preread_server_name \$kit_upstream {"
[[ -n $reality_sni ]] && echo " $reality_sni 127.0.0.1:${INNER[reality]};"
@@ -1500,7 +1500,7 @@ setup_nginx() {
echo "}"
} >/etc/nginx/kit-stream.conf
cat >/etc/nginx/conf.d/kit.conf <<NGX
# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске.
# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске.
server {
listen 127.0.0.1:${INNER[web]} ssl http2 proxy_protocol;
server_name _;
@@ -1588,14 +1588,14 @@ setup_subscription() {
# Без subURI панель показывает ссылку на внутренний порт 2097, до которого снаружи не достучаться.
local uri="https://$HOST:$SUB_PORT$SUB_PATH"
[[ $SINGLE == yes ]] && uri="https://$HOST$SUB_PATH"
upd=$(jq -c --arg path "$SUB_PATH" --argjson ip "$SUB_INTERNAL" --arg title "3X-UI KIT" --arg uri "$uri" '
upd=$(jq -c --arg path "$SUB_PATH" --argjson ip "$SUB_INTERNAL" --arg title "3X-UI_autoVPS" --arg uri "$uri" '
.subEnable = true | .subPath = $path | .subTitle = $title | .subListen = "127.0.0.1" | .subPort = $ip
| .subURI = $uri
| .subCertFile = "" | .subKeyFile = ""
| .subClashEnable = true | .subClashAutoDetect = true | .subJsonEnable = true | .subJsonAutoDetect = true' <<<"$all")
else
SUB_PORT=$(jq -r '.subPort // 2096' <<<"$all")
upd=$(jq -c --arg path "$SUB_PATH" --arg title "3X-UI KIT" '
upd=$(jq -c --arg path "$SUB_PATH" --arg title "3X-UI_autoVPS" '
.subEnable = true | .subPath = $path | .subTitle = $title
| .subClashEnable = true | .subClashAutoDetect = true | .subJsonEnable = true | .subJsonAutoDetect = true' <<<"$all")
fi
@@ -1616,7 +1616,7 @@ setup_subscription() {
kit_sub_unit() { # путь-к-сертификату путь-к-ключу (пусто – за nginx)
cat <<UNIT
[Unit]
Description=kit-sub: подписка с учётом приложения (3X-UI KIT)
Description=kit-sub: подписка с учётом приложения (3X-UI_autoVPS)
After=network-online.target x-ui.service
Wants=network-online.target
@@ -1718,11 +1718,11 @@ safe_env() { # файл имя...
local f=$1 n re; shift
[[ -r $f ]] || die "В копии нет файла ${f##*/}."
[[ $(LC_ALL=C tr -d '\n -~' <"$f" | wc -c) -eq 0 ]] \
|| die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."
|| die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."
LC_ALL=C grep -aqvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|''))?$" "$f" \
&& die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."
&& die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."
while IFS= read -r n; do
[[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."
[[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."
case $n in
BACKUP_KIT_VERSION) re='^[0-9]{1,3}(\.[0-9]{1,3}){1,3}$' ;;
BACKUP_HOST | HOST) re='^[A-Za-z0-9.:-]{1,253}$' ;;
@@ -1740,10 +1740,10 @@ safe_env() { # файл имя...
SUB_PATH) re='^/[A-Za-z0-9_-]{1,64}/$' ;;
SINGLE) re='^(yes|no)?$' ;;
MTPROTO_INNER) re='^[0-9]{0,5}$' ;;
*) die "Это не резервная копия 3X-UI KIT или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;
*) die "Это не резервная копия 3X-UI_autoVPS или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;
esac
[[ $(env_get "$f" "$n") =~ $re ]] \
|| die "Это не резервная копия 3X-UI KIT или она повреждена (значение $n в ${f##*/}). Ничего не менял."
|| die "Это не резервная копия 3X-UI_autoVPS или она повреждена (значение $n в ${f##*/}). Ничего не менял."
done < <(LC_ALL=C grep -aoE '^[A-Z][A-Z0-9_]*' "$f")
return 0
}
@@ -1874,7 +1874,7 @@ restore_main() { # файл
tmp=$(mktemp -d)
# shellcheck disable=SC2064 # путь подставляем сразу: при выходе локальной переменной уже нет
trap "rm -rf -- '$tmp'" EXIT
python3 - "$file" "$tmp" <<'PY' || die "Это не резервная копия 3X-UI KIT или она повреждена. Ничего не менял."
python3 - "$file" "$tmp" <<'PY' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена. Ничего не менял."
import os, sys, tarfile
allowed = ("etc/x-ui/x-ui.db", "etc/x-ui/install-result.env", "etc/kit/kit.env", "etc/kit-sub/config.json", "etc/kit-sub/rules.yaml",
"etc/nginx/kit-stream.conf", "etc/nginx/conf.d/kit.conf", "var/www/kit", "root/cert/self",
@@ -1935,7 +1935,7 @@ PY
safe_env "$tmp/kit-backup.env" BACKUP_KIT_VERSION BACKUP_HOST BACKUP_SSL BACKUP_DATE
safe_env "$tmp/etc/x-ui/install-result.env" XUI_USERNAME XUI_PASSWORD XUI_PANEL_PORT XUI_WEB_BASE_PATH XUI_ACCESS_URL XUI_API_TOKEN XUI_DB_TYPE
[[ $(env_get "$tmp/etc/x-ui/install-result.env" XUI_PANEL_PORT) =~ ^[0-9]{1,5}$ ]] \
|| die "Это не резервная копия 3X-UI KIT или она повреждена (порт панели). Ничего не менял."
|| die "Это не резервная копия 3X-UI_autoVPS или она повреждена (порт панели). Ничего не менял."
if [[ -f $tmp/etc/kit/kit.env ]]; then
safe_env "$tmp/etc/kit/kit.env" HOST LINK_HOST PANEL_ON SUB_BASE SUB_PATH SUB_INTERNAL SINGLE MTPROTO_INNER
fi
@@ -1947,10 +1947,10 @@ PY
BACKUP_SSL=$(env_get "$tmp/kit-backup.env" BACKUP_SSL)
BACKUP_DATE=$(env_get "$tmp/kit-backup.env" BACKUP_DATE)
BACKUP_KIT_VERSION=$(env_get "$tmp/kit-backup.env" BACKUP_KIT_VERSION)
[[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (адрес сервера). Ничего не менял."
[[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (адрес сервера). Ничего не менял."
[[ $BACKUP_SSL =~ ^(ip|custom|none)$ ]] || die "В копии нет данных о сертификате."
[[ $BACKUP_SSL != custom || -f $tmp/root/cert/custom/fullchain.pem ]] \
|| die "Это не резервная копия 3X-UI KIT или она повреждена (нет своего сертификата). Ничего не менял."
|| die "Это не резервная копия 3X-UI_autoVPS или она повреждена (нет своего сертификата). Ничего не менял."
PANEL_SSL=$BACKUP_SSL
# Домен переезжает вместе с сервером (поменяйте A-запись), IP – нет.
if [[ $BACKUP_HOST =~ ^[0-9.]+$ ]]; then
+3 -3
View File
@@ -2,18 +2,18 @@
# Hysteria2 – https://github.com/itsnotkubrick/3X-UI_KIT
#
# Отдельный установщик Hysteria2 больше не поддерживается: Hysteria2 входит в основную
# установку 3X-UI KIT (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`
# установку 3X-UI_autoVPS (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`
# получила из подписанного релиза понятное сообщение. Ничего не ставит, не удаляет и не
# останавливает: сервер Hysteria2, установленный раньше, продолжает работать как был.
# Код выхода 1 при любом запуске (и при `__update_binary`) – старый `hy2 update` на нём
# останавливается и не заменяет команду hy2.
KIT_VERSION="1.2.4"
KIT_VERSION="1.2.7"
cat >&2 <<'MSG'
Отдельный установщик Hysteria2 больше не поддерживается.
Сервер Hysteria2 продолжает работать как раньше, но не обновляется этим скриптом.
Hysteria2 входит в основную установку 3X-UI KIT (один из 11 протоколов).
Hysteria2 входит в основную установку 3X-UI_autoVPS (один из 11 протоколов).
Если нужен только Hysteria2 без панели – официальный установщик:
bash <(curl -fsSL https://get.hy2.sh/)
MSG
+510 -4
View File
@@ -64,7 +64,7 @@ kl_need_root() { [[ ${EUID:-0} -eq 0 ]] || kl_die "нужен root"; }
kit_local_help() {
cat <<'EOF'
3X-UI KIT — дополнения форка (hacker07):
3X-UI_autoVPS — дополнения форка (hacker07):
Состояние
kit local эта справка
@@ -74,6 +74,12 @@ kit_local_help() {
kit local masking состояние маскировки (у REALITY должен быть свой домен)
kit local hardening сводка по защите сервера и что ещё можно включить
Заглушка (сайт на 443 и ответ Hysteria2 — одна и та же страница)
kit local site поставить случайный сайт из нашего набора (10 штук)
kit local site --list список сайтов с номерами
kit local site 7 поставить конкретный
kit local site --show какой сайт стоит сейчас
Защита сервера (поведение взято из StressOzz/vps-setup, сделано безопаснее)
kit local ssh 2222 добавить порт SSH (прежние продолжают работать)
kit local ssh --status какие порты SSH слушаются сейчас
@@ -227,7 +233,7 @@ EOF
printf ' (dry-run) записал бы в %s: [Socket] + ListenStream=%s (старый порт остаётся)\n' "$KL_SOCKET_DROPIN" "$new"
else
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"
fi
kl_run systemctl daemon-reload
@@ -238,7 +244,7 @@ EOF
printf ' (dry-run) записал бы в %s: Port %s (старый порт остаётся)\n' "$KL_SSHD_DROPIN" "$new"
else
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"
fi
kl_run sshd -t || { kl_warn "sshd -t не принял конфиг — убираю наш файл, ничего не перезапускаю"; kl_dry || rm -f "$KL_SSHD_DROPIN"; return 1; }
@@ -408,7 +414,7 @@ APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
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::Remove-Unused-Kernel-Packages "true";
EOF
@@ -452,6 +458,430 @@ EOF
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 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() {
case ${1:-help} in
help | -h | --help) kit_local_help ;;
@@ -464,6 +894,7 @@ kit_local_cmd() {
passwd | pass) shift; kit_local_passwd "$@" ;;
upgrades | auto) shift; kit_local_upgrades "$@" ;;
hardening | hard) shift; kit_local_hardening "$@" ;;
site | stub | stub-site) shift; kit_local_site "$@" ;;
*)
printf 'Неизвестная команда форка: %s\n' "$1" >&2
kit_local_help >&2
@@ -474,6 +905,76 @@ kit_local_cmd() {
# Точка входа: вызывается из kit.sh с аргументами командной строки.
# Если команда не наша — возвращаем 0, и kit работает ровно как раньше.
# ---------------------------------------------------------------- полная справка `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 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() {
case ${1:-} in
local)
@@ -481,6 +982,11 @@ kit_local_dispatch() {
kit_local_cmd "$@"
exit $?
;;
help | -h | --help)
# Полная справка: авторские команды + наши, по категориям.
kit_help_all
exit 0
;;
*) return 0 ;;
esac
}
+3 -2
View File
@@ -494,11 +494,11 @@ def routing_payload(kind, loaded):
else:
ips.append(v)
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"]}
else:
# 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:.*"],
"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=(",", ":"))
@@ -577,3 +577,4 @@ def main():
if __name__ == "__main__":
main()
+16 -16
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# kit – управление сервером 3X-UI KIT: пользователи, обновление, резервная копия.
# kit – управление сервером 3X-UI_autoVPS: пользователи, обновление, резервная копия.
# https://github.com/itsnotkubrick/3X-UI_KIT
#
# kit user add имя [--gb 50] [--days 30] [--devices 3]
@@ -12,7 +12,7 @@
set -Eeuo pipefail
export LC_ALL=C.UTF-8 # ширина колонок по символам, а не байтам
KIT_VERSION="1.2.4"
KIT_VERSION="1.2.7"
KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main"
# Файлы новой версии берём из её тега, а не из меняющейся ветки main.
kit_ref_raw() { echo "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$1"; }
@@ -387,7 +387,7 @@ cmd_enforce() {
limit_timer_on() {
cat >/etc/systemd/system/kit-limit.service <<'UNIT'
[Unit]
Description=3X-UI KIT: общий лимит трафика пользователей (сумма по всем записям)
Description=3X-UI_autoVPS: общий лимит трафика пользователей (сумма по всем записям)
After=x-ui.service
[Service]
@@ -397,7 +397,7 @@ TimeoutStartSec=120
UNIT
cat >/etc/systemd/system/kit-limit.timer <<'UNIT'
[Unit]
Description=3X-UI KIT: проверка общего лимита трафика раз в 5 минут
Description=3X-UI_autoVPS: проверка общего лимита трафика раз в 5 минут
[Timer]
OnBootSec=2min
@@ -462,7 +462,7 @@ cmd_del() {
# Новый ключ приходит только в релизе, подписанном старым.
KIT_SIGNERS=(
# Ключ подписи форка (владелец: hacker07). Закрытая часть — только на рабочей станции.
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI KIT fork releases (hacker07)"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)"
)
KIT_SIG_NS="3x-ui-kit-release"
KIT_SIG_ID="releases@3x-ui-kit"
@@ -496,7 +496,7 @@ update_hint() {
}
cmd_version() {
echo "3X-UI KIT $KIT_VERSION"
echo "3X-UI_autoVPS $KIT_VERSION"
echo "${D}панель 3X-UI $(/usr/local/x-ui/x-ui -v 2>/dev/null | head -1 || echo '?'), ядро Xray $(xray_version)${N}"
if auto_enabled; then echo "${D}автообновление: включено (только подписанные релизы), журнал $KIT_UPDATE_LOG${N}"
else echo "${D}автообновление: выключено, включить: kit update --auto${N}"; fi
@@ -525,7 +525,7 @@ fetch_release() { # версия каталог
return 1
fi
# Версия записана внутри подписанного файла: старый подписанный релиз под видом нового не пройдёт.
grep -qx "# 3X-UI KIT $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }
grep -qxE "# (3X-UI KIT|3X-UI_autoVPS) $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }
for f in scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh; do
sum=$(awk -v f="$f" '$2 == f {print $1}' "$d/SHA256SUMS")
[[ $sum =~ ^[0-9a-f]{64}$ ]] || { warn "В подписанном списке нет $f."; return 1; }
@@ -540,7 +540,7 @@ fetch_release() { # версия каталог
kit_sub_unit() { # путь-к-сертификату путь-к-ключу (пусто – за nginx)
cat <<UNIT
[Unit]
Description=kit-sub: подписка с учётом приложения (3X-UI KIT)
Description=kit-sub: подписка с учётом приложения (3X-UI_autoVPS)
After=network-online.target x-ui.service
Wants=network-online.target
@@ -604,7 +604,7 @@ needs_migration() {
auto_on() {
cat >/etc/systemd/system/kit-update.service <<UNIT
[Unit]
Description=3X-UI KIT: автообновление kit и kit-sub (только подписанные релизы)
Description=3X-UI_autoVPS: автообновление kit и kit-sub (только подписанные релизы)
After=network-online.target
Wants=network-online.target
@@ -616,7 +616,7 @@ StandardError=append:$KIT_UPDATE_LOG
UNIT
cat >/etc/systemd/system/kit-update.timer <<'UNIT'
[Unit]
Description=3X-UI KIT: проверка обновлений раз в сутки
Description=3X-UI_autoVPS: проверка обновлений раз в сутки
[Timer]
OnCalendar=*-*-* 03:00:00
@@ -666,7 +666,7 @@ cmd_update() {
[[ $unattended == yes ]] || echo "${D}Переустановить файлы kit той же версии: kit update --force${N}"
return
fi
if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI KIT $KIT_VERSION → $latest"; fi
if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI_autoVPS $KIT_VERSION → $latest"; fi
tmp=$(mktemp -d)
# shellcheck disable=SC2064 # путь подставляем сразу: при выходе локальной переменной уже нет
trap "rm -rf -- '$tmp'" EXIT
@@ -728,7 +728,7 @@ cmd_update() {
# Новые пути подписки (Clash и JSON) подключает уже новая версия kit.
/usr/local/bin/kit __subextra >/dev/null 2>&1 || warn "Дополнительные пути подписки не подключились: kit update --force"
echo
echo "${G}✓ Готово: 3X-UI KIT $latest.${N} Пользователи, ссылки и подписки не менялись."
echo "${G}✓ Готово: 3X-UI_autoVPS $latest.${N} Пользователи, ссылки и подписки не менялись."
[[ $unattended == yes ]] && return
local news
news=$(changelog_of "$latest")
@@ -1914,10 +1914,10 @@ YAML
# Приложениям на Xray правила раздаёт сама панель (3X-UI 3.9+): заголовок Routing для Happ и правила JSON-подписки.
# Собираем их из того же rules.yaml. Чужой профиль, уже стоящий в панели, не трогаем.
split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI KIT"
split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI_autoVPS"
local v=$1
[[ $v == happ://routing/onadd/* ]] && v=$(printf '%s' "${v#happ://routing/onadd/}" | base64 -d 2>/dev/null || true)
grep -q '"Name":"3X-UI KIT"' <<<"$v"
grep -q '"Name":"3X-UI_autoVPS"' <<<"$v"
}
split_panel_sync() { # on|off
@@ -2551,7 +2551,7 @@ menu_main() {
local c
while :; do
echo
box "3X-UI KIT $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \
box "3X-UI_autoVPS $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \
"4. Обновить kit и подписку" "5. Обновить панель 3X-UI" "6. Автообновление: включить или выключить" "" \
"7. Резервная копия" "8. Версии" "" "9. Панель 3X-UI (меню x-ui)" "0. Выход"
echo
@@ -2577,7 +2577,7 @@ menu_main() {
usage() {
cat <<EOF
${B}3X-UI KIT $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})
${B}3X-UI_autoVPS $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})
kit меню
kit user … add, list, link, limit, on, off, del
+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 содержит все команды (авторские и наши)"
+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
+82 -1
View File
@@ -25,6 +25,9 @@ import sys
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")
# README.md форка — НАШ файл целиком: авторское описание из него убрано, чтобы никто не
# ставил сервер авторской командой и не оказался на авторском канале обновлений. Шва здесь нет:
# при обновлении из авторского проекта README конфликтует и решается в нашу пользу (см. FORK.md).
HOOK = """
# --- 3X-UI_KIT (форк hacker07): локальные команды и хуки ---
# Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются.
@@ -57,7 +60,7 @@ def main():
args.remove("--check")
root = pathlib.Path(args[0] if args else ".").resolve()
if not (root / "VERSION").exists():
die("%s не похож на репозиторий 3X-UI KIT (нет VERSION)" % root)
die("%s не похож на репозиторий 3X-UI_autoVPS (нет VERSION)" % root)
os.chdir(root)
version = (root / "VERSION").read_text(encoding="utf-8").strip()
@@ -172,6 +175,13 @@ def main():
"FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh scripts/3x-ui-local.sh VERSION)",
"наш файл в подписанном списке релиза",
),
(
"tools/release.sh",
'{ echo "# 3X-UI KIT $v"; echo "# 3X-UI_autoVPS $v"; sha "${FILES[@]}"; } >SHA256SUMS',
False,
"{ echo \"# 3X-UI KIT $v\"; echo \"# 3X-UI_autoVPS $v\"; sha \"${FILES[@]}\" | awk '{ sub(/^\\*/, \"\", $2); print $1\" \"$2 }'; } >SHA256SUMS",
"суммы в POSIX-виде без звёздочки (на Windows sha256sum её ставит, и kit её не понимает)",
),
# --- свои записи о версии (наш файл, авторский CHANGELOG не трогаем) ----
(
"tools/release.sh",
@@ -195,6 +205,27 @@ def main():
' { curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null; curl -fsS -m 5 "$KIT_RAW/FORK-CHANGELOG.md" 2>/dev/null; } | awk -v v="## v$1" \'',
"«что нового» ищет и в наших записях",
),
(
"scripts/3x-ui.sh",
" _ _ _ _ _ _ _\n (_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __\n | | __/ __| '_ \\ / _ \\| __| |/ / | | | '_ \\| '__| |/ __| |/ /\n | | |_\\__ \\ | | | (_) | |_| <| |_| | |_) | | | | (__| <\n |_|\\__|___/_| |_|\\___/ \\__|_|\\_\\\\__,_|_.__/|_| |_|\\___|_|\\_\\",
False,
" _ _ _ ___ _____\n | | | | __ _ ___| | _____ _ __ / _ \\___ |\n | |_| |/ _` |/ __| |/ / _ \\ '__| | | | | / /\n | _ | (_| | (__| < __/ | | |_| |/ /\n |_| |_|\\__,_|\\___|_|\\_\\___|_| \\___//_/",
"баннер сервера: наша надпись вместо авторской",
),
(
"scripts/3x-ui.sh",
' echo "${B}3X-UI KIT $KIT_VERSION${N} · it\'s not Kubrick. it\'s just a VPN."',
False,
' echo "${B}3X-UI_autoVPS $KIT_VERSION${N} · сборка форка: панель 3X-UI и VPN-протоколы на одном порту"',
"подпись под баннером: наше имя сборки",
),
(
"scripts/kit.sh",
' grep -qx "# 3X-UI KIT $v" "$d/SHA256SUMS"',
False,
' grep -qxE "# (3X-UI KIT|3X-UI_autoVPS) $v" "$d/SHA256SUMS"',
"проверка шапки подписанного релиза принимает и старое, и новое имя",
),
]
bad = 0
@@ -229,6 +260,56 @@ def main():
p.write_text(text, encoding="utf-8", newline="")
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 check:
die("якорей не найдено: %d — автор изменил код, пересмотри правки в tools/fork_apply.py" % bad)
+14 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Подпись релиза 3X-UI KIT. Запускает только автор, на своём компьютере, перед тегом:
# Подпись релиза 3X-UI_autoVPS. Запускает только автор, на своём компьютере, перед тегом:
#
# tools/release.sh [~/.ssh/3xui_kit_release]
#
# Проверяет, что версии сходятся и установщик 3X-UI не подменили, пишет SHA256SUMS,
# подписывает его закрытым ключом (ssh-keygen спросит пароль ключа) и сразу проверяет
# подпись открытым ключом, записанным в kit. Сервер 3X-UI KIT ставит обновление, только
# подпись открытым ключом, записанным в kit. Сервер 3X-UI_autoVPS ставит обновление, только
# если эта проверка у него тоже сошлась. Закрытый ключ никуда не отправляется.
set -euo pipefail
@@ -16,7 +16,7 @@ ok() { printf '✓ %s\n' "$*"; }
sha() { if command -v sha256sum >/dev/null; then sha256sum "$@"; else shasum -a 256 "$@"; fi; }
key=${1:-$HOME/.ssh/3xui_kit_release}
[[ -f $key ]] || die "Нет закрытого ключа $key. Создать: ssh-keygen -t ed25519 -C \"3X-UI KIT releases\" -f $key"
[[ -f $key ]] || die "Нет закрытого ключа $key. Создать: ssh-keygen -t ed25519 -C \"3X-UI_autoVPS releases\" -f $key"
NS="3x-ui-kit-release"
ID="releases@3x-ui-kit"
FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh scripts/3x-ui-local.sh VERSION)
@@ -29,6 +29,16 @@ 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)"
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 отстала от набора команд"
[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит."
# Установщик 3X-UI: хеш в скрипте должен совпадать с тем, что сейчас лежит на GitHub.
@@ -70,7 +80,7 @@ pub=$(ssh-keygen -y -f "$key" | awk '{print $1, $2}')
signers scripts/kit.sh | awk '{print $1, $2}' | grep -qxF "$pub" || die "Ключа $key нет в KIT_SIGNERS – серверы не примут такую подпись."
ok "Ключ подписи записан в kit"
{ echo "# 3X-UI KIT $v"; sha "${FILES[@]}"; } >SHA256SUMS
{ echo "# 3X-UI KIT $v"; echo "# 3X-UI_autoVPS $v"; sha "${FILES[@]}" | awk '{ sub(/^\*/, "", $2); print $1" "$2 }'; } >SHA256SUMS
rm -f SHA256SUMS.sig
ssh-keygen -Y sign -f "$key" -n "$NS" SHA256SUMS >/dev/null
tmp=$(mktemp)
+2 -2
View File
@@ -113,7 +113,7 @@ class RestoreEnv(unittest.TestCase):
def bad(self, over):
r = restore_env(over)
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):
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)
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)]))
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"):
+2 -2
View File
@@ -102,7 +102,7 @@ class SafeEnv(unittest.TestCase):
def rejected(self, name, text):
code, out = run(name, text, 'echo "RESULT=$RESULT"\n')
self.assertNotEqual(code, 0, out)
self.assertIn("Это не резервная копия 3X-UI KIT", out)
self.assertIn("Это не резервная копия 3X-UI_autoVPS", out)
self.assertNotIn("\nOK\n", "\n" + out)
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)
out = (r.stdout + r.stderr).decode("utf-8", "replace")
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)
def test_substitutions(self):
+1 -1
View File
@@ -128,7 +128,7 @@ class Rules(unittest.TestCase):
link = ks.routing_payload("happ", loaded)
self.assertTrue(link.startswith("happ://routing/onadd/"))
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["ProxySites"], ["geosite:youtube", "domain:example.org", "full:api.example.com"])
self.assertEqual(h["ProxyIp"], ["geoip:telegram", "203.0.113.0/24"])