1106 lines
63 KiB
Bash
1106 lines
63 KiB
Bash
#!/usr/bin/env bash
|
||
# 3X-UI_KIT (форк hacker07) — локальные дополнения.
|
||
#
|
||
# Этот файл наш: автор его не знает и не правит, поэтому обновления автора его не ломают.
|
||
# kit подключает его одной строкой из kit.sh (шов в tools/fork_apply.py) и передаёт сюда
|
||
# аргументы командной строки. Всё, что мы дописываем к kit, живёт ЗДЕСЬ.
|
||
#
|
||
# Команды: kit local <команда> (справка: kit local help)
|
||
#
|
||
# Часть команд повторяет поведение проекта StressOzz/vps-setup (добавить порт SSH, отключить
|
||
# ответ на ping, сменить пароль root). Там нет файла лицензии, поэтому код свой, а поведение
|
||
# сделано безопаснее, чем в источнике:
|
||
# * конфиг sshd проверяется (`sshd -t`) ДО перезапуска, новый порт сначала открывается в ufw;
|
||
# * прежний порт продолжает работать, пока новый не проверен (`--close-old` — отдельный шаг);
|
||
# * у каждой правки есть --dry-run и копия затронутого файла в /root/kit-local-backup-<дата>/;
|
||
# * apt full-upgrade из источника НЕ выполняется (он тянет систему вперёд и может сломать
|
||
# закреплённые версии панели и ядра) — вместо него автообновления безопасности.
|
||
#
|
||
# Правила работы с файлом: только функции, никакого кода на верхнем уровне (он выполняется при
|
||
# каждом запуске kit), и никаких зависимостей от функций автора: наш вызов идёт в самом начале
|
||
# kit.sh, когда авторские say/warn ещё не объявлены.
|
||
|
||
KL_BACKUP_DIR="/root/kit-local-backup-$(date +%Y%m%d)"
|
||
KL_SSHD_CONF="/etc/ssh/sshd_config"
|
||
KL_SSHD_DROPIN="/etc/ssh/sshd_config.d/99-kit-local-port.conf"
|
||
KL_SOCKET_DROPIN="/etc/systemd/system/ssh.socket.d/99-kit-local-port.conf"
|
||
KL_UFW_RULES=/etc/ufw/before.rules
|
||
KL_UFW6_RULES=/etc/ufw/before6.rules
|
||
KL_SYSCTL_DROPIN=/etc/sysctl.d/99-kit-local-icmp.conf
|
||
|
||
kl_say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
|
||
kl_ok() { printf ' ✓ %s\n' "$*"; }
|
||
kl_warn() { printf ' ! %s\n' "$*" >&2; }
|
||
kl_step() { printf '\n\033[1m%s\033[0m\n' "$*"; }
|
||
kl_die() { printf '✗ %s\n' "$*" >&2; return 1; }
|
||
kl_have() { command -v "$1" >/dev/null 2>&1; }
|
||
kl_dry() { [[ ${KL_DRY:-no} == yes ]]; }
|
||
kl_run() { if kl_dry; then printf ' (dry-run) %s\n' "$*"; return 0; fi; "$@"; }
|
||
|
||
# Разбор аргументов: --dry-run поднимаем в KL_DRY, остальное складываем в KL_REST.
|
||
kl_args() {
|
||
KL_DRY=no; KL_REST=()
|
||
local a
|
||
for a in "$@"; do
|
||
case $a in
|
||
--dry-run) KL_DRY=yes ;;
|
||
*) KL_REST+=("$a") ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
kl_backup() { # файл → копия рядом с релизом, не в /tmp (там её переживёт только до перезагрузки)
|
||
[[ -e $1 ]] || return 0
|
||
local dst="$KL_BACKUP_DIR/$(printf '%s' "$1" | tr '/' '_')"
|
||
if kl_dry; then printf ' (dry-run) сохранил бы %s → %s\n' "$1" "$dst"; return 0; fi
|
||
install -d -m 700 "$KL_BACKUP_DIR"
|
||
cp -p "$1" "$dst" && printf ' ✓ копия: %s\n' "$dst"
|
||
return 0
|
||
}
|
||
|
||
kl_need_root() { [[ ${EUID:-0} -eq 0 ]] || kl_die "нужен root"; }
|
||
|
||
# ------------------------------------------------------------------ справка
|
||
|
||
kit_local_help() {
|
||
cat <<'EOF'
|
||
3X-UI_autoVPS — дополнения форка (hacker07):
|
||
|
||
Состояние
|
||
kit local эта справка
|
||
kit local version версия kit, источник обновлений, дата сборки нашего файла
|
||
kit local channel откуда берутся обновления и чем проверяется подпись
|
||
kit local seams какие места форк правит в файлах автора
|
||
kit local masking состояние маскировки (у REALITY должен быть свой домен)
|
||
kit local hardening сводка по защите сервера и что ещё можно включить
|
||
kit local uninstall снести всё, что поставила установка (SSH не трогает)
|
||
|
||
Заглушка (сайт на 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 слушаются сейчас
|
||
kit local ssh --close-old 2222 закрыть прежние порты после проверки нового
|
||
kit local ping off|on отключить/вернуть ответ на ICMP echo (ping) через ufw
|
||
kit local ping status состояние ответа на ping
|
||
kit local passwd [юзер] сменить пароль (спросит дважды, в логи не попадает)
|
||
kit local passwd --generate сгенерировать пароль и показать один раз
|
||
kit local upgrades on|off|status автообновления безопасности
|
||
|
||
У любой правки можно добавить --dry-run: покажет план и ничего не изменит.
|
||
Копии изменённых файлов — в /root/kit-local-backup-<дата>/.
|
||
|
||
Оговорки:
|
||
* прежний порт SSH сам не закрывается: сначала проверьте новый из отдельного окна;
|
||
* ping отключаем правилом ufw (видно в `ufw status`), а не общесистемным sysctl: отменяется
|
||
одной командой и не глушит диагностику для внутренних интерфейсов;
|
||
* apt full-upgrade из источника не делаем: вместо него — обновления безопасности.
|
||
EOF
|
||
}
|
||
|
||
kit_local_version() {
|
||
printf 'kit: %s (форк hacker07)\n' "${KIT_VERSION:-?}"
|
||
printf 'источник: %s\n' "${KIT_RAW:-?}"
|
||
local lib=${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}
|
||
printf 'наш файл: %s\n' "$lib"
|
||
[[ -r $lib ]] && printf 'собран: %s\n' "$(date -r "$lib" '+%Y-%m-%d %H:%M' 2>/dev/null || echo '?')"
|
||
local latest
|
||
latest=$(cat /etc/kit/latest-version 2>/dev/null || true)
|
||
[[ -n $latest ]] && printf 'последняя: %s\n' "$latest"
|
||
return 0
|
||
}
|
||
|
||
kit_local_channel() {
|
||
printf 'файлы релиза: %s\n' "${KIT_RAW:-?}"
|
||
printf 'подпись: id=%s namespace=%s\n' "${KIT_SIG_ID:-?}" "${KIT_SIG_NS:-?}"
|
||
local n=0 k
|
||
for k in "${KIT_SIGNERS[@]:-}"; do [[ -n $k ]] && n=$((n + 1)); done
|
||
printf 'ключей в KIT_SIGNERS: %d (должен быть только ключ форка)\n' "$n"
|
||
if [[ $n -eq 0 ]]; then
|
||
printf 'ВНИМАНИЕ: список ключей пуст — обновление не пройдёт проверку подписи.\n'
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
kit_local_seams() {
|
||
local bin=/usr/local/bin/kit
|
||
[[ -r $bin ]] || { printf 'нет %s\n' "$bin"; return 1; }
|
||
grep -n '3X-UI_KIT (форк hacker07)' "$bin" 2>/dev/null \
|
||
|| printf 'метки форка в %s не найдены — сборка не наша?\n' "$bin"
|
||
return 0
|
||
}
|
||
|
||
kit_local_masking() {
|
||
kl_have kit || { printf 'нет команды kit\n'; return 1; }
|
||
kit net site 2>/dev/null || printf 'посмотреть: kit net site\n'
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------ SSH: порты
|
||
|
||
kl_ssh_ports() {
|
||
{ sshd -T 2>/dev/null | awk '$1 == "port" {print $2}'
|
||
kl_have ss && ss -H -ltnp 2>/dev/null | awk '/sshd/ {n = split($4, a, ":"); print a[n]}'
|
||
[[ -n ${SSH_CONNECTION:-} ]] && awk '{print $4}' <<<"$SSH_CONNECTION"
|
||
[[ -n ${SSH_CLIENT:-} ]] && awk '{print $2}' <<<"$SSH_CLIENT"
|
||
} 2>/dev/null | grep -E '^[0-9]+$' | sort -un | tr '\n' ' '
|
||
}
|
||
|
||
kl_ssh_socket_active() { kl_have systemctl && systemctl is-active --quiet ssh.socket 2>/dev/null; }
|
||
|
||
kl_ufw_active() { kl_have ufw && ufw status 2>/dev/null | grep -q '^Status: active'; }
|
||
|
||
kl_ssh_status() {
|
||
local ports; ports=$(kl_ssh_ports)
|
||
printf 'порты SSH сейчас: %s\n' "${ports:-не удалось определить}"
|
||
if kl_ssh_socket_active; then
|
||
printf 'слушает: systemd (ssh.socket — так в Ubuntu 24.04)\n'
|
||
[[ -f $KL_SOCKET_DROPIN ]] && grep -H 'ListenStream' "$KL_SOCKET_DROPIN"
|
||
else
|
||
printf 'слушает: sshd по %s\n' "$KL_SSHD_CONF"
|
||
[[ -f $KL_SSHD_DROPIN ]] && grep -H '^Port' "$KL_SSHD_DROPIN"
|
||
fi
|
||
kl_ufw_active && printf 'ufw: %s\n' "$(ufw status 2>/dev/null | awk '$2 == "ALLOW" && $1 ~ /\/tcp$/ {printf "%s ", $1}')"
|
||
return 0
|
||
}
|
||
|
||
kit_local_ssh() {
|
||
kl_args "$@"
|
||
local arg=${KL_REST[0]:-} second=${KL_REST[1]:-} close_old=no new
|
||
[[ $arg == --close-old ]] && { close_old=yes; new=$second; }
|
||
[[ $second == --close-old ]] && close_old=yes
|
||
|
||
if [[ -z $arg || $arg == --status ]]; then kl_ssh_status; return 0; fi
|
||
kl_need_root || return 1
|
||
[[ $close_old == no ]] && new=$arg
|
||
[[ $new =~ ^[0-9]+$ ]] && ((new >= 1 && new <= 65535)) \
|
||
|| { kl_die "порт — число 1..65535, например: kit local ssh 2222"; return 1; }
|
||
|
||
local cur; cur=$(kl_ssh_ports)
|
||
|
||
if [[ $close_old == yes ]]; then
|
||
kl_say "закрываю прежние порты SSH, оставляю только $new"
|
||
printf ' было: %s\n' "${cur:-?}"
|
||
if [[ -z $cur ]]; then
|
||
kl_warn "не удалось определить текущие порты SSH — закройте прежний порт вручную (файл sshd или ssh.socket)"
|
||
return 1
|
||
fi
|
||
if kl_ssh_socket_active; then
|
||
cat <<EOF
|
||
! SSH слушает systemd (ssh.socket): закрытие старых портов делается правкой
|
||
$KL_SOCKET_DROPIN — первой строкой там должен идти «ListenStream=» (сброс списка),
|
||
затем «ListenStream=$new», после чего: systemctl daemon-reload && systemctl restart ssh.socket.
|
||
Автоматически не делаю: ошибка здесь запирает доступ к серверу.
|
||
EOF
|
||
return 0
|
||
fi
|
||
local p
|
||
for p in $cur; do
|
||
[[ $p == "$new" ]] && continue
|
||
kl_backup "$KL_SSHD_CONF"
|
||
if kl_dry; then
|
||
printf ' (dry-run) закрыл бы порт %s в %s и снял правило ufw %s/tcp\n' "$p" "$KL_SSHD_CONF" "$p"
|
||
continue
|
||
fi
|
||
sed -i -E "s|^[[:space:]]*Port[[:space:]]+$p[[:space:]]*$|# Port $p (закрыт kit local $(date +%F))|" "$KL_SSHD_CONF"
|
||
kl_ufw_active && ufw delete allow "$p/tcp" >/dev/null 2>&1
|
||
kl_ok "порт $p закрыт"
|
||
done
|
||
kl_run sshd -t || { kl_warn "sshd -t не принял конфиг — верните файл из $KL_BACKUP_DIR"; return 1; }
|
||
kl_run systemctl restart ssh
|
||
kl_say "остался только порт $new. Проверьте вход в НОВОМ окне, это не закрывая."
|
||
return 0
|
||
fi
|
||
|
||
kl_say "добавляю порт SSH $new (прежние: ${cur:-?})"
|
||
if [[ " $cur " == *" $new "* ]]; then kl_warn "порт $new уже слушается — ничего не меняю"; return 0; fi
|
||
if kl_have ss && ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$new\$"; then
|
||
kl_die "порт $new занят другой службой — выберите другой"; return 1
|
||
fi
|
||
|
||
if kl_ufw_active; then
|
||
kl_run ufw allow "$new/tcp" && kl_ok "ufw: открыт $new/tcp — сделано до перезапуска, чтобы не запереть себя"
|
||
else
|
||
kl_warn "ufw не активен: проверьте, что порт $new открыт у хостера"
|
||
fi
|
||
|
||
if kl_ssh_socket_active; then
|
||
kl_backup "$KL_SOCKET_DROPIN"
|
||
if kl_dry; then
|
||
printf ' (dry-run) записал бы в %s: [Socket] + ListenStream=%s (старый порт остаётся)\n' "$KL_SOCKET_DROPIN" "$new"
|
||
else
|
||
install -d -m 755 "$(dirname "$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
|
||
kl_run systemctl restart ssh.socket
|
||
else
|
||
kl_backup "$KL_SSHD_DROPIN"
|
||
if kl_dry; then
|
||
printf ' (dry-run) записал бы в %s: Port %s (старый порт остаётся)\n' "$KL_SSHD_DROPIN" "$new"
|
||
else
|
||
install -d -m 755 /etc/ssh/sshd_config.d
|
||
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; }
|
||
kl_run systemctl restart ssh || kl_run systemctl restart sshd
|
||
fi
|
||
|
||
if ! kl_dry; then
|
||
sleep 2
|
||
if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$new\$"; then kl_ok "sshd слушает $new"
|
||
else kl_warn "порт $new пока не слушается — смотрите: journalctl -u ssh -n 30"; fi
|
||
fi
|
||
cat <<EOF
|
||
|
||
Дальше обязательно:
|
||
1) во ВТОРОМ окне проверьте вход: ssh -p $new root@<IP сервера>
|
||
2) заработало — закройте прежние порты: kit local ssh --close-old $new
|
||
3) поменяйте порт у себя в ~/.ssh/config и в мониторинге, если он там есть
|
||
EOF
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------ ping (ICMP echo)
|
||
|
||
kl_ping_state() {
|
||
if kl_have ufw && [[ -f $KL_UFW_RULES ]]; then
|
||
grep -qE '^-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT' "$KL_UFW_RULES" && echo allow || echo deny
|
||
elif [[ -f $KL_SYSCTL_DROPIN ]]; then
|
||
grep -q 'icmp_echo_ignore_all = 1' "$KL_SYSCTL_DROPIN" && echo deny || echo allow
|
||
else
|
||
echo "?"
|
||
fi
|
||
}
|
||
|
||
kit_local_ping() {
|
||
kl_args "$@"
|
||
local want=${KL_REST[0]:-status}
|
||
case $want in
|
||
status | "")
|
||
printf 'ответ на ping: %s\n' "$(kl_ping_state)"
|
||
if kl_have ufw && [[ -f $KL_UFW_RULES ]]; then
|
||
printf ' ufw before.rules: echo-request %s\n' "$(grep -qE '^# -A ufw-before-input -p icmp --icmp-type echo-request' "$KL_UFW_RULES" && echo 'закрыт (строка закомментирована)' || echo 'открыт')"
|
||
[[ -f $KL_UFW6_RULES ]] && printf ' ufw before6.rules: echo-request %s\n' "$(grep -qE '^# -A ufw6-before-input' "$KL_UFW6_RULES" && echo 'закрыт' || echo 'открыт')"
|
||
fi
|
||
printf ' sysctl icmp_echo_ignore_all=%s\n' "$(sysctl -n net.ipv4.icmp_echo_ignore_all 2>/dev/null || echo '?')"
|
||
return 0 ;;
|
||
off | on) ;;
|
||
*) kl_die "kit local ping off|on|status"; return 1 ;;
|
||
esac
|
||
kl_need_root || return 1
|
||
|
||
if kl_ufw_active; then
|
||
kl_backup "$KL_UFW_RULES"; kl_backup "$KL_UFW6_RULES"
|
||
if kl_dry; then
|
||
printf ' (dry-run) %s в %s и before6.rules правило «icmp echo-request», затем ufw reload\n' \
|
||
"$([[ $want == off ]] && echo 'закомментировал бы' || echo 'вернул бы')" "$KL_UFW_RULES"
|
||
return 0
|
||
fi
|
||
local f
|
||
for f in "$KL_UFW_RULES" "$KL_UFW6_RULES"; do
|
||
[[ -f $f ]] || continue
|
||
if [[ $want == off ]]; then
|
||
sed -i -E 's|^(-A ufw6?-before-input -p icmp6? --icmp6?-type echo-request -j ACCEPT)|# \1 # kit local: закрыт|' "$f"
|
||
else
|
||
sed -i -E 's|^# (-A ufw6?-before-input -p icmp6? --icmp6?-type echo-request -j ACCEPT).*$|\1|' "$f"
|
||
fi
|
||
done
|
||
ufw reload >/dev/null 2>&1 || kl_warn "ufw reload не прошёл — проверьте: ufw status"
|
||
rm -f "$KL_SYSCTL_DROPIN"
|
||
if [[ $want == off ]]; then
|
||
kl_ok "сервер больше не отвечает на ping"
|
||
kl_warn "ICMP-мониторинг этого сервера из дома работать перестанет (проба «ping» даст сбой)"
|
||
kl_warn "вернуть: kit local ping on"
|
||
else
|
||
kl_ok "сервер снова отвечает на ping"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
kl_warn "ufw не активен — использую общесистемный запрет ICMP echo (это шире: глушит ping и для внутренних интерфейсов)"
|
||
if kl_dry; then
|
||
printf ' (dry-run) записал бы %s с icmp_echo_ignore_all=%s\n' "$KL_SYSCTL_DROPIN" "$([[ $want == off ]] && echo 1 || echo 0)"
|
||
return 0
|
||
fi
|
||
printf 'net.ipv4.icmp_echo_ignore_all = %s\n' "$([[ $want == off ]] && echo 1 || echo 0)" >"$KL_SYSCTL_DROPIN"
|
||
sysctl -p "$KL_SYSCTL_DROPIN" >/dev/null 2>&1
|
||
kl_ok "готово (файл $KL_SYSCTL_DROPIN); вернуть: kit local ping on"
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------ пароль
|
||
|
||
kit_local_passwd() {
|
||
kl_args "$@"
|
||
local user=${KL_REST[0]:-root} gen=no
|
||
[[ $user == --generate ]] && { user=root; gen=yes; }
|
||
[[ ${KL_REST[1]:-} == --generate ]] && gen=yes
|
||
kl_need_root || return 1
|
||
id "$user" >/dev/null 2>&1 || { kl_die "нет пользователя $user"; return 1; }
|
||
|
||
if [[ $gen == yes ]]; then
|
||
if kl_dry; then printf ' (dry-run) сгенерировал бы пароль пользователю %s и показал бы один раз\n' "$user"; return 0; fi
|
||
local pw; pw=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-20)
|
||
printf '%s:%s\n' "$user" "$pw" | chpasswd || { kl_die "chpasswd не сработал"; return 1; }
|
||
cat <<EOF
|
||
|
||
Пароль пользователя $user сменён. Показываю ОДИН раз:
|
||
|
||
$pw
|
||
|
||
Перенесите в CREDENTIALS.md и очистите прокрутку терминала (clear).
|
||
EOF
|
||
else
|
||
kl_say "меняю пароль $user — спросит дважды, в историю команд не попадёт"
|
||
if kl_dry; then printf ' (dry-run) запустил бы passwd %s\n' "$user"; return 0; fi
|
||
passwd "$user" || return 1
|
||
fi
|
||
kl_dry || passwd -S "$user" 2>/dev/null
|
||
sshd -T 2>/dev/null | grep -qi '^passwordauthentication no' \
|
||
&& kl_warn "в sshd вход по паролю выключен — пароль пригодится только для консоли хостера"
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------ автообновления безопасности
|
||
|
||
kit_local_upgrades_status() {
|
||
printf 'автообновления безопасности: %s\n' "$([[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && echo включены || echo выключены)"
|
||
kl_have systemctl && printf ' служба unattended-upgrades: %s\n' "$(systemctl is-active unattended-upgrades 2>/dev/null || echo 'нет')"
|
||
[[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && sed -n '1,4p' /etc/apt/apt.conf.d/20auto-upgrades | sed 's/^/ /'
|
||
if [[ -f /var/log/unattended-upgrades/unattended-upgrades.log ]]; then
|
||
printf ' последние запуски:\n'
|
||
tail -n 3 /var/log/unattended-upgrades/unattended-upgrades.log | cut -c1-110 | sed 's/^/ /'
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
kit_local_upgrades() {
|
||
kl_args "$@"
|
||
local want=${KL_REST[0]:-status}
|
||
case $want in
|
||
status | "") kit_local_upgrades_status; return 0 ;;
|
||
on | off) ;;
|
||
*) kl_die "kit local upgrades on|off|status"; return 1 ;;
|
||
esac
|
||
kl_need_root || return 1
|
||
|
||
if [[ $want == off ]]; then
|
||
kl_say "выключаю автообновления безопасности"
|
||
kl_run rm -f /etc/apt/apt.conf.d/20auto-upgrades /etc/apt/apt.conf.d/52-kit-local-upgrades
|
||
kl_run systemctl disable --now unattended-upgrades
|
||
kl_ok "готово (пакет оставлен)"
|
||
return 0
|
||
fi
|
||
|
||
kl_say "включаю автообновления безопасности (только security, без перезагрузки)"
|
||
if ! kl_dry && ! dpkg -s unattended-upgrades >/dev/null 2>&1; then
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unattended-upgrades \
|
||
|| { kl_die "пакет не поставился"; return 1; }
|
||
fi
|
||
if kl_dry; then
|
||
printf ' (dry-run) записал бы /etc/apt/apt.conf.d/20auto-upgrades и 52-kit-local-upgrades, включил бы службу\n'
|
||
return 0
|
||
fi
|
||
cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
|
||
APT::Periodic::Update-Package-Lists "1";
|
||
APT::Periodic::Unattended-Upgrade "1";
|
||
APT::Periodic::Download-Upgradeable-Packages "1";
|
||
APT::Periodic::AutocleanInterval "7";
|
||
EOF
|
||
cat >/etc/apt/apt.conf.d/52-kit-local-upgrades <<'EOF'
|
||
// 3X-UI_autoVPS (форк): только обновления безопасности, никакой самовольной перезагрузки.
|
||
Unattended-Upgrade::Automatic-Reboot "false";
|
||
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
||
EOF
|
||
systemctl enable --now unattended-upgrades >/dev/null 2>&1 || kl_warn "служба не включилась — проверьте: systemctl status unattended-upgrades"
|
||
kl_ok "включено: обновления безопасности ставятся сами"
|
||
kl_ok "панель и ядро это не затрагивает — они ставятся не из apt, а из своих релизов"
|
||
kl_warn "обновление ядра применится только после перезагрузки, сам сервер не перезагружается"
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------ сводка
|
||
|
||
kit_local_hardening() {
|
||
kl_args "$@"
|
||
local ports; ports=$(kl_ssh_ports)
|
||
kl_step "Защита сервера — состояние"
|
||
printf 'порты SSH: %s\n' "${ports:-?}$(kl_ssh_socket_active && echo ' — через systemd ssh.socket')"
|
||
printf 'ответ на ping: %s\n' "$(kl_ping_state)"
|
||
printf 'ufw: %s\n' "$(kl_ufw_active && echo 'активен' || echo 'не активен')"
|
||
kl_ufw_active && printf 'открытые порты: %s\n' "$(ufw status 2>/dev/null | awk '$2 == "ALLOW" {printf "%s ", $1}')"
|
||
printf 'автообновления: %s\n' "$([[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && echo включены || echo выключены)"
|
||
printf 'вход по паролю в sshd: %s\n' "$(sshd -T 2>/dev/null | awk '/^passwordauthentication/ {print $2}' || echo '?')"
|
||
printf 'смена пароля root: %s\n' "$(chage -l root 2>/dev/null | awk -F': ' '/Last password change/ {print $2}')"
|
||
printf 'продление сертификата: %s\n' "$(crontab -l 2>/dev/null | grep -c acme) задач acme в cron"
|
||
|
||
cat <<'EOF'
|
||
|
||
Чего здесь НЕТ и почему (в отличие от vps-setup):
|
||
* apt full-upgrade — тянет систему вперёд и может сломать закреплённые панель и ядро;
|
||
вместо него включены обновления безопасности;
|
||
* автоматическая перезагрузка после правок — сервер не должен вставать без вашего решения;
|
||
* общесистемный icmp_echo_ignore_all — правило ufw видно в `ufw status` и отменяется одной
|
||
командой, не глушит диагностику внутренних интерфейсов.
|
||
|
||
Что включить при желании:
|
||
kit local ssh 2222 добавить порт SSH
|
||
kit local ping off не отвечать на ping
|
||
kit local upgrades on обновления безопасности
|
||
kit local passwd --generate сменить пароль root
|
||
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 ;;
|
||
version | -v) kit_local_version ;;
|
||
channel) kit_local_channel ;;
|
||
seams) kit_local_seams ;;
|
||
masking | mask) kit_local_masking ;;
|
||
ssh | ssh-port) shift; kit_local_ssh "$@" ;;
|
||
ping | icmp) shift; kit_local_ping "$@" ;;
|
||
passwd | pass) shift; kit_local_passwd "$@" ;;
|
||
upgrades | auto) shift; kit_local_upgrades "$@" ;;
|
||
hardening | hard) shift; kit_local_hardening "$@" ;;
|
||
uninstall | remove) shift; kit_local_uninstall "$@" ;;
|
||
site | stub | stub-site) shift; kit_local_site "$@" ;;
|
||
*)
|
||
printf 'Неизвестная команда форка: %s\n' "$1" >&2
|
||
kit_local_help >&2
|
||
return 2
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Точка входа: вызывается из kit.sh с аргументами командной строки.
|
||
# Если команда не наша — возвращаем 0, и kit работает ровно как раньше.
|
||
# ---------------------------------------------------------------- снос установки целиком
|
||
# Убираем всё, что появляется после установки: панель 3X-UI и службу x-ui, ядро Xray, службы и файлы
|
||
# kit (kit-sub, автообновление, cron), конфиги nginx и сайт-заглушку, сертификаты, правила ufw для 80/443,
|
||
# наши правки системы (правило ICMP, обновления безопасности), наши бэкапы и файл с данными панели.
|
||
#
|
||
# ЧЕГО НЕ ТРОГАЕМ: SSH — ни порт, ни конфиги, ни правила ufw для него, ни наш ssh-дроп-ин. Иначе легко
|
||
# остаться без доступа к серверу. Если SSH-порт когда-то менялся, верните его сами: kit local ssh 22
|
||
# (и закрыть лишние: kit local ssh --close-old 22).
|
||
#
|
||
# kit local uninstall спросит подтверждение словом «удалить»
|
||
# kit local uninstall --dry-run только покажет, что будет удалено
|
||
kit_local_uninstall() {
|
||
kl_args "$@"
|
||
local arg=${KL_REST[0]:-} ans answer
|
||
case $arg in
|
||
"") ;;
|
||
--dry-run) KL_DRY=yes ;;
|
||
*) kl_die "kit local uninstall [--dry-run]"; return 1 ;;
|
||
esac
|
||
|
||
local -a svc=(x-ui kit-sub kit-update.timer kit-update.service)
|
||
local -a files=(
|
||
/usr/local/x-ui /etc/x-ui /usr/bin/x-ui /etc/systemd/system/x-ui.service
|
||
/usr/local/bin/kit /usr/local/lib/kit /etc/kit /etc/kit-sub
|
||
/etc/systemd/system/kit-sub.service /etc/systemd/system/kit-update.service
|
||
/etc/systemd/system/kit-update.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 /root/.acme.sh /etc/letsencrypt
|
||
/etc/sysctl.d/99-kit-local-icmp.conf /etc/apt/apt.conf.d/52-kit-local-upgrades
|
||
)
|
||
local -a backups=()
|
||
local b
|
||
for b in /root/kit-local-backup-*; do [[ -e $b ]] && backups+=("$b"); done
|
||
|
||
kl_step "Будет удалено"
|
||
printf ' службы: %s\n' "${svc[*]}"
|
||
printf ' файлы и каталоги: %s\n' "${files[*]}"
|
||
[[ ${#backups[@]} -eq 0 ]] || printf ' наши копии изменённых файлов: %s\n' "${backups[*]}"
|
||
printf ' правила ufw для 80/tcp и 443/tcp (правило для SSH не трогаем)\n'
|
||
printf ' ответ на ping вернём (снимем наше правило)\n'
|
||
if kl_ufw_active; then
|
||
if [[ $(grep -c '^# -A ufw-before-input -p icmp --icmp-type echo-request' /etc/ufw/before.rules 2>/dev/null || echo 0) -gt 0 ]]; then
|
||
printf ' %s\n' "правило ICMP наше (строка закомментирована) — вернём как было"
|
||
fi
|
||
fi
|
||
kl_ok "перезагрузка не потребуется, SSH и его порт не затрагиваются"
|
||
|
||
if kl_dry; then
|
||
printf '\n (dry-run) ничего не делаю. Запустить по-настоящему: kit local uninstall\n'
|
||
return 0
|
||
fi
|
||
kl_need_root || return 1
|
||
|
||
if [[ -z ${KIT_LOCAL_UNINSTALL_CONFIRMED:-} ]]; then
|
||
printf '\n%sЧтобы удалить всё перечисленное, напишите слово «удалить»: %s' "${B:-}" "${N:-}"
|
||
read -r ans </dev/tty 2>/dev/null || ans=""
|
||
[[ ${ans,,} == "удалить" ]] || { kl_warn "не подтверждено — ничего не удаляю"; return 1; }
|
||
fi
|
||
|
||
kl_step "Останавливаю и отключаю службы"
|
||
local s
|
||
for s in "${svc[@]}"; do
|
||
systemctl disable --now "$s" >/dev/null 2>&1 && kl_ok "остановлено и отключено: $s" || kl_warn "служба $s не найдена (уже удалена?)"
|
||
done
|
||
if kl_have nginx; then
|
||
local others
|
||
others=$(find /etc/nginx/conf.d /etc/nginx/sites-enabled -type f -o -type l 2>/dev/null | grep -vE 'kit\.conf$|default$' | wc -l)
|
||
if [[ $others -eq 0 ]]; then
|
||
kl_run systemctl disable --now nginx >/dev/null 2>&1 && kl_ok "nginx остановлен (своих сайтов, кроме нашего, не было)"
|
||
KL_NGINX_PURGE=yes
|
||
else
|
||
kl_ok "nginx оставляю: в нём есть другие сайты ($others) — уберу только наши конфиги"
|
||
KL_NGINX_PURGE=no
|
||
fi
|
||
fi
|
||
|
||
kl_step "Убираю файлы"
|
||
for f in "${files[@]}"; do
|
||
[[ -e $f ]] || continue
|
||
kl_run rm -rf "$f" && kl_ok "удалено: $f"
|
||
done
|
||
for f in "${backups[@]}"; do kl_run rm -rf "$f" && kl_ok "удалено: $f"; done
|
||
[[ ${KL_NGINX_PURGE:-no} == yes ]] && { kl_run apt-get remove -y --purge nginx nginx-common >/dev/null 2>&1 && kl_ok "пакет nginx удалён"; }
|
||
|
||
kl_step "Возвращаю систему"
|
||
kl_run systemctl daemon-reload
|
||
if kl_ufw_active; then
|
||
local p
|
||
for p in 80/tcp 443/tcp 443/udp; do kl_run ufw delete allow "$p" >/dev/null 2>&1 && kl_ok "правило ufw снято: $p"; done
|
||
if [[ -f $KL_UFW_RULES ]]; then
|
||
kl_backup "$KL_UFW_RULES"; kl_backup "$KL_UFW6_RULES"
|
||
kl_run sed -i 's/^# -A ufw-before-input -p icmp --icmp-type echo-request/-A ufw-before-input -p icmp --icmp-type echo-request/' "$KL_UFW_RULES"
|
||
[[ -f $KL_UFW6_RULES ]] && kl_run sed -i 's/^# -A ufw6-before-input -p icmpv6 --icmpv6-type echo-request/-A ufw6-before-input -p icmpv6 --icmpv6-type echo-request/' "$KL_UFW6_RULES"
|
||
kl_run ufw reload >/dev/null 2>&1
|
||
kl_ok "ответ на ping возвращён"
|
||
fi
|
||
fi
|
||
kl_run sysctl -q -w net.ipv4.icmp_echo_ignore_all=0 >/dev/null 2>&1 || true
|
||
|
||
kl_step "Проверка"
|
||
local left=0
|
||
for s in "${svc[@]}"; do systemctl list-unit-files 2>/dev/null | grep -q "^$s" && { kl_warn "осталась служба: $s"; left=1; }; done
|
||
command -v kit >/dev/null && { kl_warn "команда kit ещё на месте (запустите команду ещё раз или удалите /usr/local/bin/kit)"; left=1; }
|
||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE '[:.]443$' && { kl_warn "порт 443 всё ещё кем-то слушается"; left=1; }
|
||
(( left )) || kl_ok "от установки ничего не осталось, порт 443 свободен"
|
||
kl_ok "SSH не тронут: порт 22 (или ваш текущий) и правила для него на месте"
|
||
kl_step "Готово. Сервер вернулся к состоянию до установки."
|
||
}
|
||
|
||
# ---------------------------------------------------------------- полная справка `kit help`
|
||
# Категории и все команды: сначала авторские, затем наши. Если автор добавит команду, а справка
|
||
# про неё не знает — сборку релиза остановит проверка tools/check-help.sh.
|
||
kit_help_all() {
|
||
cat <<EOF
|
||
${B:-}3X-UI_autoVPS ${KIT_VERSION:-?} — все команды${N:-} (то же самое доступно в меню: kit без аргументов)
|
||
|
||
Состояние и версии
|
||
kit меню
|
||
kit version версии: сборка, панель 3X-UI, ядро Xray
|
||
kit local version наша сборка, источник обновлений, дата файла
|
||
kit local channel откуда берутся обновления и чем проверяется подпись
|
||
kit local seams какие места форк правит в файлах автора
|
||
kit help эта справка
|
||
|
||
Пользователи
|
||
kit user add <имя> [--gb N] [--days N] [--devices N] создать (пусто/0 — без ограничения)
|
||
kit user list список пользователей
|
||
kit user link <имя> [--all | --amnezia | --telegram] подписка с QR-кодом и ссылки
|
||
kit user limit <имя> --gb N --days N --devices N лимиты трафика, срока, устройств
|
||
kit user on <имя> включить пользователя
|
||
kit user off <имя> выключить пользователя
|
||
kit user del <имя> удалить
|
||
kit user enforce применить лимиты сейчас
|
||
kit user vision <имя> [on|off] поток xtls-rprx-vision у REALITY
|
||
|
||
Подключения и порты
|
||
kit net состояние подключений
|
||
kit net on <имя> включить подключение
|
||
kit net off <имя> выключить подключение
|
||
kit net port <имя> <порт> перевести подключение на другой порт
|
||
kit net vision on|off Vision у всех REALITY сразу
|
||
kit net split on|off|check|apply разделение трафика по правилам (split)
|
||
kit port какие порты слушают подключения
|
||
kit port set <имя> <порт> сменить порт подключения
|
||
kit reality add [порт] добавить второй REALITY
|
||
|
||
Маскировка и сайт
|
||
kit net site [сайт] сменить сайт маскировки (без аргумента — подобрать соседа по подсети)
|
||
kit net fp <отпечаток> отпечаток TLS (chrome, firefox, safari, …)
|
||
kit sni какие сайты маскировки у подключений
|
||
kit sni rotate сменить сайты маскировки у всех подключений
|
||
kit local masking состояние маскировки (у REALITY должен быть свой домен)
|
||
kit local site [--list | N | --show] сайт-заглушка: случайный ИТ-сайт, список, конкретный, какой стоит
|
||
|
||
Проверка и починка
|
||
kit local uninstall снести установку целиком (панель, туннель, службы kit, сайт, сертификаты); SSH не трогает
|
||
kit check [--deep | --fix] проверка сервера (--deep — ходит наружу как клиент)
|
||
kit fix [--dry-run] безопасные починки: время, службы, конфиги
|
||
kit local hardening сводка по защите и что ещё можно включить
|
||
|
||
Обслуживание и обновление
|
||
kit update [--panel | --force | --auto | --manual] обновить сборку, панель, автообновление
|
||
kit panel update обновить панель 3X-UI
|
||
kit backup копия сервера: панель, сертификаты, настройки
|
||
kit net masq on|off ответ Hysteria2 страницей сайта
|
||
kit net dns on|off DoH в подписках
|
||
|
||
Защита сервера (наши команды)
|
||
kit local ssh <порт> | --status | --close-old <порт> добавить порт SSH, какие слушают, закрыть прежние
|
||
kit local ping off | on | status ответ на ping (правилом ufw)
|
||
kit local passwd [--generate] [пользователь] сменить пароль (в логи не попадает)
|
||
kit local upgrades on | off | status обновления безопасности
|
||
|
||
Общие правила
|
||
* у наших команд есть --dry-run: показывает план и ничего не меняет
|
||
* подробная справка по нашему слою: kit local help
|
||
* копии изменённых файлов: /root/kit-local-backup-<дата>/
|
||
EOF
|
||
}
|
||
|
||
kit_local_dispatch() {
|
||
case ${1:-} in
|
||
local)
|
||
shift
|
||
kit_local_cmd "$@"
|
||
exit $?
|
||
;;
|
||
help | -h | --help)
|
||
# Полная справка: авторские команды + наши, по категориям.
|
||
kit_help_all
|
||
exit 0
|
||
;;
|
||
*) return 0 ;;
|
||
esac
|
||
}
|