diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..191eb5f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: ShellCheck + run: shellcheck -S warning -e SC1090,SC2034,SC2211 scripts/*.sh + - name: Python + run: python3 -m py_compile scripts/kit-sub.py + - name: Генераторы + run: node --test tools/test/*.test.js + - name: Версия в скриптах совпадает с VERSION + run: | + v=$(cat VERSION) + grep -q "^KIT_VERSION=\"$v\"" scripts/3x-ui.sh + grep -q "^KIT_VERSION=\"$v\"" scripts/hysteria2.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b127ce6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,45 @@ +# Что нового + +## v1.1: kit сходил к аудитору, сдал анализы и теперь не доверяет никому, даже GitHub + +Подписчик прислал нам полноценный аудит безопасности. Мы почитали, покраснели и всё починили. +Спасибо: такие письма лучше любых звёзд на GitHub (но звёзды тоже ставьте). + +**Безопасность** + +- **Hysteria2 больше не верит на слово.** Почта, адрес сервера и сайт маскировки теперь проверяются + строго, а в файл настроек пишутся с экранированием. Раньше особо изобретательная почта могла + выполниться как команда. Теперь `me@gmail.com;rm -rf` получит вежливый отказ. +- **Установщик 3X-UI сверяем по отпечатку.** Мы закрепили не только версию, но и SHA256 файла. + Если на GitHub кто-то подменит установщик, скрипт посмотрит на него как на незнакомца в чате + с «выгодным предложением» и запускать не станет. +- **Hysteria проверяется нашим ключом, а не чужим.** Контрольная сумма бинарника теперь записана + в сам скрипт, а не скачивается из того же места, что и бинарник. Проверять человека по + справке, которую он же и принёс, так себе идея. +- **Всё качаем из релиза, а не из черновиков.** `kit`, `kit-sub` и `hy2 update` берут файлы + из тега версии, а не из ветки `main`, которая может поменяться посреди установки. +- **Подписка больше не root.** `kit-sub` работает от временного пользователя без прав: конфиг + и сертификат ему выдаёт systemd на время работы. Даже если его взломают, ключей от сервера + у него нет. +- **Меню x-ui оставили в покое.** Больше не переписываем чужие файлы по расписанию, только + один раз при установке. +- **Строгие имена.** Параметры `--sni` и `--host` у установщика 3X-UI тоже проверяются. + +**Для тех, кто хочет проверить сам** + +- В README появилась установка с проверкой: скачать, прочитать, сверить SHA256, запустить. +- `SECURITY.md`: как сообщить об уязвимости, не публикуя её на весь интернет. +- Проверка кода на каждый коммит: ShellCheck, Python, тесты генераторов. + +**Как обновиться** + +- Новые установки получают всё сразу. +- Hysteria2: `hy2 update`. +- Серверу 3X-UI из версии 1.0 ничего не угрожает: критическая ошибка была только в установщике + Hysteria2 и срабатывала при установке. Перевести подписку на работу без root поможет + `kit update` в версии 1.2. Пока можно просто продолжать пользоваться. + +## v1.0 + +Первая версия: 11 протоколов одной командой, всё через 443, одна подписка на все приложения, +пользователи через `kit user`, генераторы конфигов для XKeen. diff --git a/README.md b/README.md index d16d337..83c6c3a 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,26 @@ bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main ``` Через пару минут скрипт покажет адрес панели, логин, пароль и подписку с QR-кодом. + +
+🔍 Хотите сначала посмотреть скрипт? Установка с проверкой + +Скачайте скрипт конкретной версии, прочитайте его и сверьте SHA256 с тем, что указан +на странице [релиза](https://github.com/itsnotkubrick/3X-UI_KIT/releases/latest): + +```bash +curl -fsSLO https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1/scripts/3x-ui.sh +less 3x-ui.sh +sha256sum 3x-ui.sh +bash 3x-ui.sh +``` + +Скрипт версии 1.1 скачивает свои части (`kit`, `kit-sub`) из того же релиза, а установщик +3X-UI сверяет по SHA256 и не запускает, если он изменился. Первый раз удобнее ставить на +свежий VPS без ваших данных. + +
+ Подробно — подключение приложений, дополнительные пользователи и параметры — в **[инструкции](manuals/3x-ui.md)**. Нужен только Hysteria2 — есть [отдельный скрипт](manuals/hysteria2.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4eb9100 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Безопасность + +Нашли уязвимость? Пожалуйста, не публикуйте её в открытом issue. + +- Напишите через [закрытый отчёт GitHub](https://github.com/itsnotkubrick/3X-UI_KIT/security/advisories/new) + (вкладка **Security → Report a vulnerability**) или в Telegram [@itsnotkubrick](https://t.me/itsnotkubrick). +- Опишите, где проблема (файл и строки), как её воспроизвести и чем она грозит. + +Мы ответим, исправим и отметим вас в списке изменений, если вы не против. + +## Что проект делает для безопасности + +- Установщик 3X-UI закреплён по версии **и** по SHA256: если файл на GitHub изменится, + скрипт его не запустит. +- Бинарник Hysteria сверяется с SHA256, записанным в самом скрипте. +- Свои части (`kit`, `kit-sub`, `hy2 update`) скрипт берёт из тега релиза, а не из ветки `main`. +- Подписка `kit-sub` работает без прав root: systemd `DynamicUser` и `LoadCredential`. +- Все данные, которые попадают в файлы настроек, проверяются и экранируются. +- Генераторы конфигов для роутеров работают только в браузере и никуда не отправляют ваши ссылки. + +## Ограничения + +Скрипт ставит стороннее ПО: панель 3X-UI, ядро Xray, Hysteria, nginx. Мы закрепляем их версии, +но не проверяем их исходный код целиком. Сгенерированный конфиг Mihomo скачивает списки +сайтов (rule-set) и веб-панель из их проектов на GitHub. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..9459d4b --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.1 diff --git a/scripts/3x-ui.sh b/scripts/3x-ui.sh index 50bd389..889a5d6 100755 --- a/scripts/3x-ui.sh +++ b/scripts/3x-ui.sh @@ -13,6 +13,11 @@ set -Eeuo pipefail XUI_VERSION="v3.8.5" +# SHA256 установщика 3X-UI этой версии: тег могут передвинуть, а хеш – нет (проверено 2026-09-30). +XUI_INSTALL_SHA256="4e3fe7fe00ef8e904ce6a0e9c36fd8a0c7179fe5e786f23e31801aee84c6347d" +KIT_VERSION="1.1" +# kit и kit-sub берём из того же релиза, что и этот скрипт, а не из меняющейся ветки main. +KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION" # Ядро Xray для панели. С 26.7.x клиенты на Mihomo и sing-box (Hiddify, FlClash, # Clash Verge, Mihomo в XKeen) не проходят REALITY — проверено 2026-09-25. # 26.6.27 — последняя версия, с которой работают все клиенты и которую принимает 3X-UI. @@ -128,7 +133,7 @@ main() { warn "Панель 3X-UI удалена, но остались файлы прошлой установки — убираю их." systemctl disable --now kit-sub >/dev/null 2>&1 || true rm -rf /etc/systemd/system/kit-sub.service /usr/local/lib/kit-sub /etc/kit-sub /etc/kit /usr/local/bin/kit \ - /etc/cron.d/kit-nginx-reload /etc/cron.d/kit-xui-menu "$RESULT" + /etc/cron.d/kit-nginx-reload /etc/cron.d/kit-xui-menu /etc/cron.d/kit-sub-cert "$RESULT" systemctl daemon-reload # Наш nginx держит 443 — без этого проверка порта ниже не пустит REALITY. if [[ -f /etc/nginx/kit-stream.conf ]]; then @@ -160,6 +165,9 @@ main() { esac done [[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT" + local re_host='^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$' + [[ -z $SNI || $SNI =~ $re_host ]] || die "--sni: нужно имя сайта, например dl.google.com" + [[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || die "--host: нужен IP или домен" [[ $NAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя: латиница, цифры, _ . - (до 32 символов)." [[ $PANEL_SSL =~ ^(auto|ip|none)$ ]] || die "--panel-ssl: auto, ip или none" if [[ -n $ucert || -n $ukey ]]; then @@ -234,6 +242,8 @@ main() { tmp=$(mktemp) say "Ставлю 3X-UI $XUI_VERSION официальным установщиком (пара минут)" curl -fsSL --retry 3 -o "$tmp" "https://raw.githubusercontent.com/$XUI_REPO/$XUI_VERSION/install.sh" + [[ $(sha256sum "$tmp" | awk '{print $1}') == "$XUI_INSTALL_SHA256" ]] \ + || die "Установщик 3X-UI $XUI_VERSION не совпал с проверенным (SHA256) – не запускаю. Сообщите нам: github.com/itsnotkubrick/3X-UI_KIT/issues" if ! XUI_NONINTERACTIVE=1 XUI_SSL_MODE="${PANEL_SSL/custom/none}" XUI_SERVER_IP="$HOST" \ XUI_PANEL_PORT="$panel_port" XUI_WEB_BASE_PATH="$panel_path" \ XUI_USERNAME="$panel_user" XUI_PASSWORD="$panel_pass" \ @@ -712,7 +722,7 @@ awg_attach() { # имя subId [лимит-байт] [срок-мс] [устро done } -KIT_CLI_URL="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/kit.sh" +KIT_CLI_URL="$KIT_RAW/scripts/kit.sh" install_kit_cli() { install -d -m 700 /etc/kit @@ -737,7 +747,7 @@ KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubri # После «x-ui → Uninstall» меню подсказывает команду официального установщика — # меняем её на нашу. Только в echo: вызов установщика в «Update» не трогаем. -# Меню обновляется вместе с панелью, поэтому раз в сутки подсказку правит cron. +# Правим один раз при установке, без cron: чужие файлы по расписанию не трогаем. brand_xui_menu() { printf '%s\n' '/echo.*mhsanaei\/3x-ui\/[a-z]*\/install\.sh/ s#bash <(curl -Ls https://raw\.githubusercontent\.com/mhsanaei/3x-ui/[a-z]*/install\.sh)#'"$KIT_INSTALL_CMD"'#' \ >/etc/kit/xui-menu.sed @@ -745,8 +755,7 @@ brand_xui_menu() { for f in /usr/bin/x-ui /usr/local/x-ui/x-ui.sh; do [[ -f $f ]] && sed -i -f /etc/kit/xui-menu.sed "$f" done - echo '23 4 * * * root for f in /usr/bin/x-ui /usr/local/x-ui/x-ui.sh; do [ -f "$f" ] && sed -i -f /etc/kit/xui-menu.sed "$f"; done' \ - >/etc/cron.d/kit-xui-menu + rm -f /etc/cron.d/kit-xui-menu } # ---------- всё на 443: nginx ---------- @@ -910,7 +919,44 @@ setup_subscription() { SUB_FETCH="$(if [[ $TRUSTED == yes ]]; then echo https; else echo http; fi)://$HOST:$SUB_PORT$SUB_PATH$SUBID" } -KIT_SUB_URL="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/kit-sub.py" +# Юнит kit-sub: без root (DynamicUser), конфиг и сертификат – через LoadCredential. +# Сертификат Let's Encrypt на IP продлевается раз в несколько дней, поэтому при отдельном +# порте kit-sub перезапускается раз в сутки и берёт свежий. +kit_sub_unit() { # путь-к-сертификату путь-к-ключу (пусто – за nginx) + cat </etc/kit-sub/config.json fi chmod 600 /etc/kit-sub/config.json - cat >/etc/systemd/system/kit-sub.service <<'UNIT' -[Unit] -Description=kit-sub: подписка с учётом приложения (3X-UI KIT) -After=network-online.target x-ui.service -Wants=network-online.target - -[Service] -ExecStart=/usr/bin/python3 /usr/local/lib/kit-sub/kit_sub.py -Restart=on-failure -RestartSec=5 -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=read-only -PrivateTmp=true -ProtectKernelTunables=true -ProtectKernelModules=true -ProtectControlGroups=true -CapabilityBoundingSet=CAP_NET_BIND_SERVICE -MemoryMax=64M - -[Install] -WantedBy=multi-user.target -UNIT + if [[ $SINGLE == yes ]]; then + kit_sub_unit >/etc/systemd/system/kit-sub.service + else + kit_sub_unit "$CERT" "$KEY" >/etc/systemd/system/kit-sub.service + echo '19 4 * * * root systemctl restart kit-sub >/dev/null 2>&1' >/etc/cron.d/kit-sub-cert + fi systemctl daemon-reload systemctl enable kit-sub >/dev/null 2>&1 systemctl restart kit-sub diff --git a/scripts/hysteria2.sh b/scripts/hysteria2.sh index 52a5b2f..d73c879 100755 --- a/scripts/hysteria2.sh +++ b/scripts/hysteria2.sh @@ -12,7 +12,16 @@ set -Eeuo pipefail HY_VERSION="2.12.3" HY_REPO="HyNetworks/hysteria" -SELF_URL="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/hysteria2.sh" +# SHA256 бинарников этой версии, записанные в сам скрипт: проверка не зависит от файла +# hashes.txt, который лежит там же, где бинарник (2026-09-30, hashes.txt релиза app/v2.12.3). +declare -A HY_SHA256=( + [amd64]=8c7a68a906998b747a0db87586e364f995fbfddb95693ae6e2fdb68a6e920d3e + [arm64]=c8dc653c3ba0a28d29a26b8fa52d2086f27c0927afddce95c09965e7174e78b0 +) +KIT_VERSION="1.1" +KIT_REPO_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT" +# Скрипт берём из тега релиза, а не из меняющейся ветки main. +SELF_URL="$KIT_REPO_RAW/v$KIT_VERSION/scripts/hysteria2.sh" BIN=/usr/local/bin/hysteria CLI=/usr/local/bin/hy2 @@ -94,8 +103,7 @@ install_binary() { tmp=$(mktemp -d) say "Скачиваю Hysteria $HY_VERSION ($arch) с GitHub" curl -fsSL --retry 3 -o "$tmp/hysteria" "$url/hysteria-linux-$arch" - curl -fsSL --retry 3 -o "$tmp/hashes.txt" "$url/hashes.txt" - expected=$(awk -v f="build/hysteria-linux-$arch" '$2==f {print $1}' "$tmp/hashes.txt") + expected=${HY_SHA256[$arch]:-} actual=$(sha256sum "$tmp/hysteria" | awk '{print $1}') [[ -n $expected && $expected == "$actual" ]] || { rm -rf "$tmp"; die "Контрольная сумма не совпала — файл повреждён или подменён."; } install -m 755 "$tmp/hysteria" "$BIN" @@ -277,10 +285,14 @@ cmd_install() { read -rp "Домен (Enter — без домена): " DOMAIN if [[ -n $DOMAIN ]]; then read -rp "Почта для Let's Encrypt: " EMAIL; fi fi + # Значения попадают в install.env и конфиг Hysteria — пропускаем только допустимые символы. + local re_host='^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$' if [[ -n $DOMAIN ]]; then - [[ $DOMAIN =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] || die "Похоже, это не домен: $DOMAIN" - [[ $EMAIL == *@*.* ]] || die "Для Let's Encrypt нужна почта: --email you@example.com" + [[ $DOMAIN =~ $re_host ]] || die "Похоже, это не домен: $DOMAIN" + [[ $EMAIL =~ ^[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,63}$ ]] || die "Для Let's Encrypt нужна почта: --email you@example.com" fi + [[ -z $SNI || $SNI =~ $re_host ]] || die "--sni: нужно имя сайта, например www.bing.com" + [[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || die "--host: нужен IP или домен" [[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT" [[ $USERNAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя пользователя: латиница, цифры, _ . - (до 32 символов)." @@ -310,14 +322,15 @@ cmd_install() { write_masq [[ -z $DOMAIN ]] && make_self_signed "$SNI" - cat >"$STATE" <"$STATE" chmod 600 "$STATE" printf '%s %s\n' "$USERNAME" "$(rand 16)" >"$USERS" chmod 600 "$USERS" @@ -424,9 +437,13 @@ cmd_status() { cmd_update() { require_installed local tmp + local latest + latest=$(curl -fsS -m 8 "$KIT_REPO_RAW/main/VERSION" | tr -d '[:space:]') || true + [[ $latest =~ ^[0-9]+(\.[0-9]+)+$ ]] || die "Не удалось узнать последнюю версию: GitHub недоступен с сервера." tmp=$(mktemp) - say "Скачиваю свежую версию скрипта" - curl -fsSL --retry 3 -o "$tmp" "$SELF_URL" + say "Скачиваю скрипт версии $latest" + curl -fsSL --retry 3 -o "$tmp" "$KIT_REPO_RAW/v$latest/scripts/hysteria2.sh" + bash -n "$tmp" || { rm -f "$tmp"; die "Скачанный скрипт повреждён – ничего не менял."; } bash "$tmp" __update_binary install -m 755 "$tmp" "$CLI" rm -f "$tmp" diff --git a/scripts/kit-sub.py b/scripts/kit-sub.py index dc45856..307797b 100644 --- a/scripts/kit-sub.py +++ b/scripts/kit-sub.py @@ -27,7 +27,11 @@ import urllib.request import yaml -CONFIG = os.environ.get("KIT_SUB_CONFIG", "/etc/kit-sub/config.json") +# Под systemd kit-sub работает без root (DynamicUser): конфиг и сертификат ему передаёт +# systemd через LoadCredential в $CREDENTIALS_DIRECTORY. +CREDS = os.environ.get("CREDENTIALS_DIRECTORY", "") +CONFIG = os.environ.get("KIT_SUB_CONFIG") or ( + os.path.join(CREDS, "config.json") if CREDS and os.path.exists(os.path.join(CREDS, "config.json")) else "/etc/kit-sub/config.json") CLASH_UA = re.compile(r"clash|mihomo|flclash|stash|nyanpasu|meta", re.I) # AmneziaWG добавляем только приложениям на ядре Mihomo. Karing, Hiddify и другие на sing-box # тоже могут просить формат Clash (Karing так и делает), но AmneziaWG не умеют. @@ -211,6 +215,8 @@ class Server(http.server.ThreadingHTTPServer): def main(): cert, key = CONF.get("cert"), CONF.get("key") + if cert and CREDS and os.path.exists(os.path.join(CREDS, "cert.pem")): + cert, key = os.path.join(CREDS, "cert.pem"), os.path.join(CREDS, "key.pem") if not cert: srv = Server((CONF.get("listen", "127.0.0.1"), int(CONF["port"])), Handler) log(f"kit-sub слушает http://{CONF.get('listen', '127.0.0.1')}:{CONF['port']}{PATH} (TLS снимает nginx)")