#!/usr/bin/env bash # 3X-UI_autoVPS (форк hacker07) — установка сервера одной командой из нашего канала. # # Что делает (по шагам): # 1) проверяет сервер и наличие curl, ssh-keygen, openssl; # 2) берёт из нашего Gitea релиз своей же версии, проверяет ПОДПИСЬ SHA256SUMS и SHA256 установщика; # 3) запускает установщик автора (scripts/3x-ui.sh) — панель 3X-UI, протоколы, сайт-заглушка, # маскировка, сертификат, подписка, 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/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 # # Флаги этого скрипта (остальные уходят установщику автора): # --domain ДОМЕН, --sni САЙТ, --panel-on ip|domain, --host IP, --user ИМЯ, --port N, --restore ФАЙЛ # --harden закрыть ping и включить автообновления, без вопросов # --no-harden не трогать ping и автообновления # --ssh-port N добавить порт SSH N (прежний порт продолжает работать) # --yes ни о чём не спрашивать (ping закрою, автообновления включу, порт и пароль — нет) # --no-time не запускать kit fix # --no-check не запускать в конце kit check # --clean убрать прошлую установку и поставить заново (SSH не трогает) # --auto-sni не предлагать выбор сайта маскировки (иначе покажем меню по скану подсети) # --verify-only только проверить релиз (подпись и SHA256) и выйти # --dry-run показать план и ничего не делать # -h | --help эта справка # # Наш файл: авторские релизы его не перезаписывают, а правки форка живут в scripts/kit-local.sh. set -Eeuo pipefail KIT_VERSION="1.2.1.6" # версия сборки форка; должна совпадать с 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_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=; } say() { printf '%s\n' "${G}==>${N} $*"; } warn() { printf '%s\n' "${Y}!${N} $*" >&2; } die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; } usage() { cat <<'EOF' 3X-UI_autoVPS (форк hacker07) — установка сервера одной командой из нашего канала. 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 файл Флаги этого скрипта: --harden закрыть ping и включить автообновления, без вопросов --no-harden не трогать ping и автообновления --ssh-port N перевести SSH на порт N (прежний закроется после проверки входа) --yes ни о чём не спрашивать (для автоматизации) --site N|random|off сайт-заглушка: конкретный из нашего набора, случайный (по умолчанию) или не трогать авторскую страницу (то же: --no-site) --no-sub не показывать в конце подписку, доступ к панели и QR-код --clean снести прошлую установку и поставить заново (если прошлый запуск оборвался) --auto-sni не предлагать выбор сайта маскировки (автоподбор автора) --no-time не запускать kit fix (время и безопасные починки) --no-check не запускать в конце kit check --verify-only только проверить релиз (подпись и SHA256) и выйти --dry-run показать план и ничего не делать -h | --help эта справка EOF } # ------------------------------------------------------------------ разбор флагов INSTALLER_ARGS=(); harden=auto; ssh_port=""; fix_time=yes; do_check=yes; dry=no; verify_only=no; assume_yes=no site=random # заглушка: случайный сайт из нашего набора (off — не трогать) show_sub=yes # в самом конце показать подписку, QR-код и ссылки clean=no # --clean: убрать прошлую установку перед новой pick_sni=yes # показывать меню выбора сайта маскировки по результатам скана while (($#)); do 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 ;; --clean) clean=yes ;; --auto-sni) pick_sni=no ;; --no-time) fix_time=no ;; --no-check) do_check=no ;; --verify-only) verify_only=yes ;; --dry-run) dry=yes ;; -h | --help) usage; exit 0 ;; *) INSTALLER_ARGS+=("$1") ;; esac shift done [[ -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), подпись проверяется ключом форка" [[ $clean == yes ]] && pre "прошлая установка: уберу её перед установкой (--clean), SSH не трону" if [[ $pick_sni == yes ]]; then pre "маскировка: просканирую подсеть и предложу выбрать сайт (--auto-sni — автоподбор)" else pre "маскировка: подберу сайт сам, без вопросов (--auto-sni)"; fi if ((${#INSTALLER_ARGS[@]})); then pre "установщик автора: bash scripts/3x-ui.sh ${INSTALLER_ARGS[*]}"; else pre "установщик автора: bash scripts/3x-ui.sh — спросит про маскировку и домен"; fi if [[ $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 "в самом конце: доступ к панели 3X-UI, подписка, QR-код и ссылки (--no-sub — не надо)" echo say "запуск без --dry-run выполнит всё это на сервере" exit 0 fi # ------------------------------------------------------------------ релиз: скачать и проверить подпись # Подпись проверяем ДО запуска чужого кода, поэтому здесь нужны только curl/ssh-keygen/openssl. for c in curl ssh-keygen openssl; do command -v "$c" >/dev/null || die "нет $c — поставьте: apt-get update && apt-get install -y $c"; done say "беру релиз v$KIT_VERSION и проверяю подпись" tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT raw="$GITEA/raw/tag/v$KIT_VERSION" for f in SHA256SUMS SHA256SUMS.sig; do mkdir -p "$tmp/$(dirname "$f")" curl -fsSL --retry 3 -m 60 -o "$tmp/$f" "$raw/$f" || die "не скачался $f из $raw (Gitea доступна с сервера?)" 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 '{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 установщика не совпал с подписанным — не запускаю" # Качаем все остальные файлы релиза и сверяем каждый с подписанным списком. Раньше брали только # установщик, поэтому kit-local.sh (в нём наша страница-заглушка) отсутствовал, и подготовка # заглушки до установщика падала с «kl_site_html: command not found». while read -r sum2 name2; do case $sum2 in \#* | "") continue ;; esac [[ $name2 == scripts/3x-ui.sh ]] && continue mkdir -p "$tmp/$(dirname "$name2")" curl -fsSL --retry 3 -m 60 -o "$tmp/$name2" "$raw/$name2" || die "не скачался $name2 из $raw (Gitea доступна с сервера?)" got2=$(sha256sum "$tmp/$name2" | awk '{print $1}') [[ $got2 == "$sum2" ]] || die "SHA256 не совпал у $name2 — не запускаю" done <"$tmp/SHA256SUMS" bash -n "$tmp/scripts/3x-ui.sh" || die "установщик не проходит проверку синтаксиса" [[ -r $tmp/scripts/kit-local.sh ]] || die "в релизе нет scripts/kit-local.sh — сборка неполная" printf ' ✓ подпись сошлась, SHA256 установщика совпал\n' if [[ $verify_only == yes ]]; then say "релиз v$KIT_VERSION проверен: подпись и SHA256 сошлись, установщик не запускался" exit 0 fi # ------------------------------------------------------------------ предполётные проверки say "проверяю сервер" [[ ${EUID:-0} -eq 0 ]] || die "нужен root: запустите от root (на чистом VPS обычно так и есть)" [[ -r /etc/os-release ]] || die "не вижу /etc/os-release — это не Debian/Ubuntu?" # shellcheck disable=SC1091 . /etc/os-release case ${ID:-} in debian | ubuntu | raspbian) ;; *) warn "система ${PRETTY_NAME:-?}: поддерживаются Debian и Ubuntu" ;; esac printf ' %s\n' "${PRETTY_NAME:-Linux}, ядро $(uname -r), памяти $(awk '/MemTotal/ {printf "%.1f ГБ", $2 / 1048576}' /proc/meminfo 2>/dev/null || echo '?')" 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[*]}")" } # ------------------------------------------------------------------ снос прошлой установки (--clean) # Нужен, когда прошлая установка оборвалась: авторский установщик в таком случае отказывается работать # («3X-UI уже установлена этим скриптом»). Убираем то, что создаёт установка, и ставим заново. # SSH не трогаем: ни порт, ни конфиги — иначе можно потерять доступ к серверу. clean_previous() { say "убираю прошлую установку (--clean)" local s p for s in x-ui kit-sub kit-update.timer kit-update.service kit-limit.timer kit-limit.service \ $(systemctl list-unit-files --no-legend 2>/dev/null | awk '{print $1}' | grep -E '^(x-ui|kit-|kit\.)' | grep -E '\.(service|timer)$'); do systemctl list-unit-files 2>/dev/null | grep -q "^$s" || continue systemctl disable --now "$s" >/dev/null 2>&1 || true printf ' остановлено: %s\n' "$s" done for p in /usr/local/x-ui /etc/x-ui /usr/bin/x-ui /usr/bin/x-ui.new /etc/systemd/system/x-ui.service \ /usr/local/bin/kit /usr/local/lib/kit /usr/local/lib/kit-sub /etc/kit /etc/kit-sub \ /etc/systemd/system/kit-sub.service /etc/systemd/system/kit-update.service \ /etc/systemd/system/kit-update.timer /etc/systemd/system/kit-limit.service \ /etc/systemd/system/kit-limit.timer \ /etc/cron.d/kit-nginx-reload /etc/cron.d/kit-xui-menu /etc/cron.d/kit-sub-cert \ /etc/nginx/kit-stream.conf /etc/nginx/conf.d/kit.conf /var/www/kit \ /root/3x-ui.txt /root/cert /var/log/kit-install-check.log \ /root/kit-local-backup-* /root/kit-backup-* /root/x-ui-before-sni-* /root/x-ui-before-update-*; do [[ -e $p ]] || continue rm -rf $p && printf ' удалено: %s\n' "$p" done if [[ -f /etc/nginx/nginx.conf ]] && grep -q 'kit-stream\.conf' /etc/nginx/nginx.conf; then sed -i '/kit-stream\.conf/d' /etc/nginx/nginx.conf printf ' из nginx.conf убрана наша строка включения\n' fi if id -u kit-sub >/dev/null 2>&1; then pkill -u kit-sub >/dev/null 2>&1 || true userdel -r kit-sub >/dev/null 2>&1 || true printf ' удалён пользователь kit-sub\n' fi systemctl daemon-reload if [[ -f /root/3x-ui.txt && -x /usr/local/x-ui/x-ui ]]; then warn "часть файлов прошлой установки осталась — проверьте: ls -la /usr/local/x-ui /root/3x-ui.txt" else printf ' готово: место под установку свободно\n' fi } installer_failed() { if grep -q "likely firewall problem" /var/log/kit-domain-cert.log 2>/dev/null \ || grep -q "Timeout during connect" /var/log/3x-ui-install.log 2>/dev/null; then printf '\n%s!%s %s\n' "$Y" "$N" \ "Let's Encrypt не смог достучаться до порта 80 (в логе acme «Timeout during connect»)." >&2 printf ' Проверьте по порядку: открыт ли 80 в ufw (скрипт делает это сам) и не режет ли его хостер.\n' >&2 printf ' Либо ставьте без своего домена: в меню маскировки выберите «общедоступный сайт» —\n' >&2 printf ' сертификат для домена тогда не нужен, панель останется на самоподписанном.\n' >&2 fi if [[ -f /root/3x-ui.txt && -x /usr/local/x-ui/x-ui ]]; then printf '\n%s!%s %s\n' "$Y" "$N" \ "На сервере осталась прошлая установка (установщик так и пишет). Запустите ещё раз с флагом --clean — он уберёт старое и поставит заново." >&2 fi die "установщик остановился с ошибкой — смотрите его вывод выше" } # ------------------------------------------------------------------ установка if [[ -z $proto_flag ]]; then protos=$(ask_protocols) INSTALLER_ARGS+=(--protocols "$protos") else protos=$proto_flag fi say "протоколы: $protos — под остальные не создаётся ничего" # ------------------------------------------------------------------ вопросы (всё спросим ДО установки) # После установщика на экране появляются доступы к панели: там вопросы уже неуместны. Поэтому # спрашиваем всё заранее, а действия выполняем после установки (см. «наши дополнения» ниже). ask_yes() { # "вопрос" y|n — по умолчанию local q=$1 def=$2 ans="" interactive || { [[ $def == y ]]; return; } printf '\n%s [%s]: ' "$q" "$([[ $def == y ]] && echo 'Y/n' || echo 'y/N'])" read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true ans=${ans,,}; ans=${ans//[[:space:]]/} [[ -z $ans && $def == y ]] && return 0 [[ $ans =~ ^(y|yes|д|да)$ ]] } # порт SSH: по флагу или по вопросу. Смена делается в два шага: сначала ДОБАВИТЬ новый порт # (прежний продолжает работать), потом закрыть прежние — иначе одна опечатка оставляет сервер без доступа. ssh_ports_now() { local p p=$(kit local ssh --status 2>/dev/null | sed -n 's/^порты SSH сейчас: //p') [[ -n $p ]] || p=$(sshd -T 2>/dev/null | awk '$1 == "port" {print $2}' | paste -sd, -) [[ -n $p ]] || p=$(grep -rhiE '^[[:space:]]*Port[[:space:]]+[0-9]+' /etc/ssh/sshd_config /etc/ssh/sshd_config.d 2>/dev/null | awk '{print $2}' | paste -sd, -) printf '%s' "${p:-22}" } ask_ssh_port() { # пусто — оставить как есть; иначе номер нового порта local cur ans try cur=$(ssh_ports_now) interactive || return 0 for try in 1 2 3; do printf '\n%sСменить порт SSH? Сейчас %s. Введите новый номер, Enter — оставить как есть: %s' "$B" "$cur" "$N" >&2 ans="" read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true ans=${ans//[[:space:]]/} [[ -n $ans ]] || { say "порт SSH оставляю как есть ($cur)" >&2; return 0; } if ! [[ $ans =~ ^[0-9]{1,5}$ ]] || ((10#$ans < 1024 || 10#$ans > 65535)); then warn "«$ans» не похоже на порт (нужно число 1024–65535)" continue fi if [[ ,$cur, == *",$ans,"* ]]; then say "порт $ans уже слушает SSH — менять нечего" >&2; return 0 fi if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$ans\$"; then warn "порт $ans/tcp занят другой программой" continue fi printf '%s' "$ans"; return 0 done warn "оставляю прежний порт ($cur)" } want_ping=$harden; want_update=$harden [[ $want_ping == auto ]] && want_ping=yes [[ $want_update == auto ]] && want_update=yes if [[ $harden == auto ]] && interactive; then if ask_yes "Закрыть ответ сервера на ping? Снаружи пинг перестанет отвечать (подписка и VPN не затрагиваются)." y; then want_ping=yes; else want_ping=no; fi if ask_yes "Включить автообновления безопасности? Только security-обновления, без перезагрузки." y; then want_update=yes; else want_update=no; fi fi [[ -n $ssh_port ]] || ssh_port=$(ask_ssh_port) ssh_ports_before=$(ssh_ports_now) # для подсказки в конце: какие порты закрывать want_passwd=no if interactive; then if ask_yes "Сменить пароль root прямо сейчас? Новый пароль введёте в следующем окне." n; then want_passwd=yes; fi fi [[ $clean == yes ]] && clean_previous # Порты для сертификата и маскировки: открываем правила ufw ДО установщика и говорим, где смотреть, # если сертификат не выпустится (частая причина — свой файрвол у хостера, а не ufw). if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "^Status: active"; then for _p in 80/tcp 443/tcp; do ufw allow "$_p" >/dev/null 2>&1 || true; done say "порты 80 и 443 открыты в ufw (нужны для сертификата и работы маскировки)" fi printf ' %sесли у хостера свой файрвол — 80 и 443 должны быть разрешены и там.%s\n' "$D" "$N" printf ' %sпризнак закрытого 80: сертификат не выпускается, в логе acme «Timeout during connect (likely firewall problem)»%s\n' "$D" "$N" # Меню выбора сайта маскировки: сканируем подсеть один раз и предлагаем выбрать сайт отдельно для # REALITY и (если выбраны) для XHTTP/MTProto. Отключается флагом --auto-sni (тогда автоподбор автора). if [[ $pick_sni == yes ]]; then export KL_PICK_SNI=1; else export KL_PICK_SNI=0; fi say "запускаю установщик автора (панель 3X-UI, протоколы, сайт-заглушка, подписка)" if ((${#INSTALLER_ARGS[@]})); then printf ' флаги: %s\n' "${INSTALLER_ARGS[*]}"; else printf ' флагов нет: установщик задаст вопросы (маскировка, домен)\n'; fi # stdin: если есть терминал — отдаём его установщику (там есть вопросы), иначе /dev/null, # иначе установщик начнёт читать наш собственный скрипт вместо ответов. if [[ -r /dev/tty ]]; then bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} /dev/null || die "после установки нет команды kit — что-то пошло не так, смотрите вывод выше" # Ставим нашу заглушку СРАЗУ (а не поверх авторской): страницу кладём до установщика, а его собственную # страницу из релиза убираем — иначе она перезапишет нашу на шаге сборки сайта. stub_staged=no if [[ $site != off ]]; then stub_n="" case $site in random) stub_n=$(( RANDOM % 10 )) ;; *) [[ $site =~ ^[0-9]+$ ]] && stub_n=$((10#$site - 1)) ;; esac if [[ -n $stub_n ]]; then install -d -m 755 /var/www/kit if ( source "$tmp/scripts/kit-local.sh" 2>/dev/null; kl_site_html "$stub_n" ) >/var/www/kit/index.html 2>"$tmp/stub.err" \ && [[ -s /var/www/kit/index.html ]]; then stub_staged=yes rm -f "$tmp/var/www/kit/index.html" say "заглушка: наш сайт №$((stub_n + 1)) — ставится сразу, авторская страница не используется" else warn "не смог подготовить нашу заглушку — поставлю после установки (kit local site). Причина: $(head -c 200 "$tmp/stub.err" 2>/dev/null | tr '\n' ' ')" fi fi fi # Запоминаем, какие протоколы выбраны: наши команды (kit local site и др.) смотрят сюда и не # делают ничего для протоколов, которых в этой установке нет. printf '%s\n' "${protos//,/ }" | tr ' ' '\n' | sed '/^$/d' > /etc/kit/kl-protos 2>/dev/null || true # ------------------------------------------------------------------ наши дополнения (только действия) # Ничего не спрашиваем: все ответы собраны до установщика. Здесь только выполняем. # 443 нужен для маскировки и VPN, 80 — для выпуска сертификата: проверяем правила ufw ещё раз. if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "^Status: active"; then for _p in 80/tcp 443/tcp; do ufw allow "$_p" >/dev/null 2>&1 || true; done fi # Сайт-заглушка: у автора это случайные «кофейни и пекарни», у нас — правдоподобный ИТ-сайт. # Меняем не файл вручную, а командой форка: она же обновляет копию страницы в ответе Hysteria2, # иначе 443/tcp и UDP-порт начнут отвечать разными страницами (это видно снаружи). case ${stub_staged:-no} in yes) say "заглушка уже стоит (поставлена при установке, Hysteria2 синхронизирован тем же файлом)" ;; *) case $site in off) say "заглушку не трогаю (--no-site): остаётся страница автора" ;; random) say "ставлю сайт-заглушку: случайный ИТ-сайт из нашего набора" kit local site || warn "не удалось поставить заглушку — позже: kit local site" ;; *) say "ставлю сайт-заглушку №$site" kit local site "$site" || warn "не удалось поставить заглушку — позже: kit local site $site" ;; esac ;; esac if [[ $fix_time == yes ]]; then say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)" kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод" fi # Действия по ответам, которые были заданы ДО установщика (см. блок вопросов выше). if [[ $want_ping == yes ]]; then say "закрываю ответ на ping (правилом ufw)" kit local ping off || warn "не удалось — повторите: kit local ping off" fi if [[ $want_update == yes ]]; then say "включаю автообновления безопасности" kit local upgrades on || warn "не удалось — повторите: kit local upgrades on" fi if [[ $want_passwd == yes ]]; then say "меняю пароль root" kit local passwd || warn "не получилось — повторите: kit local passwd" fi # порт SSH: новый порт добавляем, прежний остаётся рабочим до вашей проверки входа. if [[ -n $ssh_port ]]; then say "перевожу SSH на порт $ssh_port: новый добавляю, прежний пока остаётся рабочим" kit local ssh "$ssh_port" || warn "порт не добавился — сделайте отдельно: kit local ssh $ssh_port" fi # ------------------------------------------------------------------ итог: коротко и по делу if [[ $do_check == yes || $show_sub == yes ]]; then # kit check печатает много строк: человеку нужны только замечания и одна строка «всё ли в порядке». chk=$(kit check 2>&1 || true) problems=$(printf '%s\n' "$chk" | grep -cE '^❌|^⚠|^!' || true) [[ $problems == 0 ]] || printf '%s\n' "$chk" | grep -E '^❌|^⚠|^!' | sed 's/^/ /' ports_open="" command -v ufw >/dev/null 2>&1 && ports_open=$(ufw status 2>/dev/null | awk '$2 == "ALLOW" { printf "%s ", $1 }') ip_show=$(hostname -I 2>/dev/null | awk '{ print $1 }') os_show=$( . /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-}" ) cert_show="без сертификата: панель открывается через SSH-туннель" if [[ -s /root/cert/ip/fullchain.pem ]]; then if openssl x509 -in /root/cert/ip/fullchain.pem -noout -issuer 2>/dev/null | grep -q "Let's Encrypt"; then cert_show="${G}Let's Encrypt на IP — доверенный, браузер не ругается${N}" else cert_show="${Y}самоподписанный — браузер предупредит${N}" fi fi protos_show=$(tr '\n' ' ' /dev/null || true) cat </dev/null; then grep -aE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt | sed 's/^[[:space:]]*/ /' printf ' файл целиком (там же подписка и ссылки по протоколам): /root/3x-ui.txt\n' else warn "данных панели в /root/3x-ui.txt нет — смотрите вывод установщика выше" fi cat <} 2) закройте прежние порты: kit local ssh --close-old $ssh_port EOF fi cat <