605 lines
40 KiB
Bash
605 lines
40 KiB
Bash
#!/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.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_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 нет установщика — релиз собран неверно"
|
||
# Качаем ВСЕ файлы релиза и сверяем каждый с подписанным списком. Так у нас есть и scripts/3x-ui.sh
|
||
# (установщик), и scripts/kit-local.sh (наша страница-заглушка), и всё остальное. Раньше брали только
|
||
# установщик: заглушка не готовилась («kl_site_html: command not found»), а проверка целостности была неполной.
|
||
while read -r sum2 name2; do
|
||
case $sum2 in \#* | "") continue ;; esac
|
||
case $name2 in SHA256SUMS | SHA256SUMS.sig) continue ;; esac
|
||
mkdir -p "$tmp/$(dirname "$name2")"
|
||
curl -fsSL --retry 3 -m 60 -o "$tmp/$name2" "$raw/$name2" || die "не скачался $name2 из $raw (Gitea доступна с сервера?)"
|
||
got2=$(sha256sum "$tmp/$name2" | awk '{print $1}')
|
||
[[ $got2 == "$sum2" ]] || die "SHA256 не совпал у $name2 — не запускаю"
|
||
done <"$tmp/SHA256SUMS"
|
||
got=$(sha256sum "$tmp/scripts/3x-ui.sh" | awk '{print $1}')
|
||
[[ $got == "$sum" ]] || die "SHA256 установщика не совпал с подписанным — не запускаю"
|
||
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/tty || installer_failed
|
||
else
|
||
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/null || installer_failed
|
||
fi
|
||
command -v kit >/dev/null || die "после установки нет команды kit — что-то пошло не так, смотрите вывод выше"
|
||
|
||
# Ставим нашу заглушку СРАЗУ (а не поверх авторской): страницу кладём до установщика, а его собственную
|
||
# страницу из релиза убираем — иначе она перезапишет нашу на шаге сборки сайта.
|
||
stub_staged=no
|
||
if [[ $site != off ]]; then
|
||
stub_n=""
|
||
case $site in
|
||
random) stub_n=$(( RANDOM % 10 )) ;;
|
||
*) [[ $site =~ ^[0-9]+$ ]] && stub_n=$((10#$site - 1)) ;;
|
||
esac
|
||
if [[ -n $stub_n ]]; then
|
||
install -d -m 755 /var/www/kit
|
||
if ( source "$tmp/scripts/kit-local.sh" 2>/dev/null; kl_site_html "$stub_n" ) >/var/www/kit/index.html 2>"$tmp/stub.err" \
|
||
&& [[ -s /var/www/kit/index.html ]]; then
|
||
stub_staged=yes
|
||
rm -f "$tmp/var/www/kit/index.html"
|
||
say "заглушка: наш сайт №$((stub_n + 1)) — ставится сразу, авторская страница не используется"
|
||
else
|
||
warn "не смог подготовить нашу заглушку — поставлю после установки (kit local site). Причина: $(head -c 200 "$tmp/stub.err" 2>/dev/null | tr '\n' ' ')"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Запоминаем, какие протоколы выбраны: наши команды (kit local site и др.) смотрят сюда и не
|
||
# делают ничего для протоколов, которых в этой установке нет.
|
||
printf '%s\n' "${protos//,/ }" | tr ' ' '\n' | sed '/^$/d' > /etc/kit/kl-protos 2>/dev/null || true
|
||
|
||
# ------------------------------------------------------------------ наши дополнения (только действия)
|
||
|
||
# Ничего не спрашиваем: все ответы собраны до установщика. Здесь только выполняем.
|
||
|
||
# 443 нужен для маскировки и VPN, 80 — для выпуска сертификата: проверяем правила ufw ещё раз.
|
||
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "^Status: active"; then
|
||
for _p in 80/tcp 443/tcp; do ufw allow "$_p" >/dev/null 2>&1 || true; done
|
||
fi
|
||
|
||
# Сайт-заглушка: у автора это случайные «кофейни и пекарни», у нас — правдоподобный ИТ-сайт.
|
||
# Меняем не файл вручную, а командой форка: она же обновляет копию страницы в ответе Hysteria2,
|
||
# иначе 443/tcp и UDP-порт начнут отвечать разными страницами (это видно снаружи).
|
||
case ${stub_staged:-no} in
|
||
yes) say "заглушка уже стоит (поставлена при установке, Hysteria2 синхронизирован тем же файлом)" ;;
|
||
*) case $site in
|
||
off) say "заглушку не трогаю (--no-site): остаётся страница автора" ;;
|
||
random)
|
||
say "ставлю сайт-заглушку: случайный ИТ-сайт из нашего набора"
|
||
kit local site || warn "не удалось поставить заглушку — позже: kit local site"
|
||
;;
|
||
*)
|
||
say "ставлю сайт-заглушку №$site"
|
||
kit local site "$site" || warn "не удалось поставить заглушку — позже: kit local site $site"
|
||
;;
|
||
esac ;;
|
||
esac
|
||
|
||
if [[ $fix_time == yes ]]; then
|
||
say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)"
|
||
kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод"
|
||
fi
|
||
|
||
# Действия по ответам, которые были заданы ДО установщика (см. блок вопросов выше).
|
||
if [[ $want_ping == yes ]]; then
|
||
say "закрываю ответ на ping (правилом ufw)"
|
||
kit local ping off || warn "не удалось — повторите: kit local ping off"
|
||
fi
|
||
if [[ $want_update == yes ]]; then
|
||
say "включаю автообновления безопасности"
|
||
kit local upgrades on || warn "не удалось — повторите: kit local upgrades on"
|
||
fi
|
||
|
||
if [[ $want_passwd == yes ]]; then
|
||
say "меняю пароль root"
|
||
kit local passwd || warn "не получилось — повторите: kit local passwd"
|
||
fi
|
||
|
||
# порт SSH: новый порт добавляем, прежний остаётся рабочим до вашей проверки входа.
|
||
if [[ -n $ssh_port ]]; then
|
||
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' ' ' </etc/kit/kl-protos 2>/dev/null || true)
|
||
cat <<EOF
|
||
|
||
${B}────────────── Установка завершена ──────────────${N}
|
||
сборка: 3X-UI_autoVPS $KIT_VERSION (канал: наш Gitea)
|
||
сервер: ${ip_show:-?}${os_show:+ · $os_show}
|
||
протоколы: ${protos_show:-—}
|
||
сертификат: $cert_show
|
||
проверка: $([[ $problems == 0 ]] && echo 'замечаний нет' || echo "замечаний: $problems (подробности: kit check)")
|
||
${B}───────────────────────────────────────────────────${N}
|
||
|
||
${B}Доступ к панели${N} (файл /root/3x-ui.txt, виден только root)
|
||
EOF
|
||
# Автор пишет эти строки без ведущих пробелов (RESULT=/root/3x-ui.txt), поэтому ищем их как есть.
|
||
if grep -aqE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt 2>/dev/null; then
|
||
grep -aE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt | sed 's/^[[:space:]]*/ /'
|
||
printf ' файл целиком (там же подписка и ссылки по протоколам): /root/3x-ui.txt\n'
|
||
else
|
||
warn "данных панели в /root/3x-ui.txt нет — смотрите вывод установщика выше"
|
||
fi
|
||
cat <<EOF
|
||
|
||
${B}Ссылки для подключения и QR-код${N} — в меню kit
|
||
kit → Пользователи → Показать ссылки из консоли: kit local links
|
||
EOF
|
||
if [[ -n $ports_open ]]; then
|
||
cat <<EOF
|
||
|
||
${B}Порты${N}
|
||
открыто: $ports_open
|
||
что нужно и что лишнее: kit local ports закрыть лишние: kit local ports --close-extra
|
||
EOF
|
||
fi
|
||
if [[ -n ${ssh_port:-} ]]; then
|
||
cat <<EOF
|
||
|
||
${Y}SSH переведён на порт $ssh_port.${N} Прежние (${ssh_ports_before:-22}) пока открыты намеренно:
|
||
1) проверьте вход во втором окне: ssh -p $ssh_port root@${ip_show:-<IP>}
|
||
2) закройте прежние порты: kit local ssh --close-old $ssh_port
|
||
EOF
|
||
fi
|
||
cat <<EOF
|
||
|
||
если что-то не так: kit check --deep, затем kit fix
|
||
EOF
|
||
fi
|