390 lines
17 KiB
Bash
Executable file
390 lines
17 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
readonly CONFIG_DIR="/etc/ochenstarik-server"
|
||
readonly MANAGED_PORTS_CONFIG="${CONFIG_DIR}/ufw-managed-ports.conf"
|
||
readonly IP_FAMILY_CONFIG="${CONFIG_DIR}/ip-family.conf"
|
||
readonly SSH_PORT_CONFIG="${CONFIG_DIR}/ssh-port.conf"
|
||
readonly IPV6_SYSCTL_FILE="/etc/sysctl.d/99-zz-ochenstarik-disable-ipv6.conf"
|
||
readonly SWAPFILE="/swapfile"
|
||
readonly SWAP_SYSCTL_FILE="/etc/sysctl.d/60-hermes-swap.conf"
|
||
readonly SSHD_DROPIN="/etc/ssh/sshd_config.d/00-hermes-hardening.conf"
|
||
readonly FAIL2BAN_JAIL="/etc/fail2ban/jail.d/hermes.local"
|
||
readonly SETUP_ENV="/root/setup-data/env.txt"
|
||
readonly SETUP_HASH="/root/setup-data/password.hash"
|
||
readonly TELEGRAM_CONFIG="${CONFIG_DIR}/telegram.conf"
|
||
readonly TELEGRAM_SCRIPT="/usr/local/libexec/ochenstarik-ssh-login-telegram.sh"
|
||
readonly TELEGRAM_LEGACY_SCRIPT="/usr/local/libexec/ssh-login-telegram.sh"
|
||
readonly TELEGRAM_LOG="/var/log/ochenstarik-ssh-login-telegram.log"
|
||
readonly TELEGRAM_LOGROTATE="/etc/logrotate.d/ochenstarik-ssh-login-telegram"
|
||
readonly PAM_SSHD="/etc/pam.d/sshd"
|
||
readonly XRAY_STATE_DIR="/etc/ochenstarik-xray"
|
||
readonly XRAY_CONFIG_DIR="/usr/local/etc/xray"
|
||
readonly XRAY_SHARE_DIR="/usr/local/share/xray"
|
||
readonly XRAY_LOG_DIR="/var/log/xray"
|
||
readonly XRAY_HELPER="/usr/local/sbin/ochenstarik-xray-routing"
|
||
readonly XRAY_SERVICE="/etc/systemd/system/ochenstarik-xray-routing.service"
|
||
readonly BACKUP_CONFIG="${CONFIG_DIR}/backup.conf"
|
||
readonly BACKUP_RUNNER="/usr/local/sbin/ochenstarik-server-backup"
|
||
readonly LOCALE_BACKUP="${CONFIG_DIR}/locale-before-selection.conf"
|
||
readonly LOCALE_ABSENT_MARKER="${CONFIG_DIR}/locale-before-selection.absent"
|
||
readonly LOCALE_PACKAGES="${CONFIG_DIR}/locale-installed-packages.list"
|
||
readonly LEGACY_LOCALE_BACKUP="${CONFIG_DIR}/locale-before-russian.conf"
|
||
readonly LEGACY_LOCALE_ABSENT_MARKER="${CONFIG_DIR}/locale-before-russian.absent"
|
||
readonly LEGACY_LOCALE_PACKAGES="${CONFIG_DIR}/russian-locale-installed-packages.list"
|
||
readonly PROGRAM_PACKAGES="${CONFIG_DIR}/step1-installed-packages.list"
|
||
|
||
log() { printf '[+] %s\n' "$*"; }
|
||
warn() { printf '[!] %s\n' "$*" >&2; }
|
||
die() { printf '[x] %s\n' "$*" >&2; exit 1; }
|
||
|
||
ask_yes_no() {
|
||
local prompt="$1" default_answer="${2:-no}" answer suffix
|
||
[[ "$default_answer" == yes || "$default_answer" == no ]] \
|
||
|| die "Некорректный ответ по умолчанию"
|
||
[[ "$default_answer" == yes ]] && suffix='[Y/n]' || suffix='[y/N]'
|
||
while :; do
|
||
read -rp "$prompt $suffix " answer || die "Ввод прерван"
|
||
answer="${answer:-$default_answer}"
|
||
case "${answer,,}" in
|
||
y|yes|д|да) return 0 ;;
|
||
n|no|н|нет) return 1 ;;
|
||
*) warn "Ответьте yes/no или да/нет" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
service_exists() {
|
||
command -v systemctl >/dev/null 2>&1 && systemctl cat "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
stop_disable_service() {
|
||
local unit="$1"
|
||
service_exists "$unit" || return 0
|
||
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||
}
|
||
|
||
is_valid_port() {
|
||
[[ "$1" =~ ^[0-9]{1,5}$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535))
|
||
}
|
||
|
||
current_ssh_port() {
|
||
local port=""
|
||
if [[ -n "${SSH_CONNECTION:-}" ]]; then
|
||
port="${SSH_CONNECTION##* }"
|
||
is_valid_port "$port" && printf '%s' "$((10#$port))"
|
||
fi
|
||
}
|
||
|
||
remove_ufw_rules() {
|
||
local rule port protocol active_port
|
||
command -v ufw >/dev/null 2>&1 || return 0
|
||
active_port="$(current_ssh_port)"
|
||
|
||
if LANG=C ufw status 2>/dev/null | grep -q '^Status: active'; then
|
||
ufw allow 22/tcp >/dev/null
|
||
log "Порт 22/tcp открыт перед сбросом SSH"
|
||
fi
|
||
|
||
[[ -f "$MANAGED_PORTS_CONFIG" && ! -L "$MANAGED_PORTS_CONFIG" ]] || return 0
|
||
while IFS= read -r rule || [[ -n "$rule" ]]; do
|
||
[[ "$rule" =~ ^([0-9]{1,5})/(tcp|udp)$ ]] || continue
|
||
port="$((10#${BASH_REMATCH[1]}))"
|
||
protocol="${BASH_REMATCH[2]}"
|
||
if [[ "$protocol" == tcp && "$port" == 22 ]]; then
|
||
continue
|
||
fi
|
||
if [[ -n "$active_port" && "$protocol" == tcp && "$port" == "$active_port" ]]; then
|
||
warn "Правило текущего SSH-порта ${port}/tcp оставлено для защиты соединения"
|
||
continue
|
||
fi
|
||
while ufw --force delete allow from 0.0.0.0/0 to any port "$port" proto "$protocol" \
|
||
>/dev/null 2>&1; do :; done
|
||
while ufw --force delete allow from ::/0 to any port "$port" proto "$protocol" \
|
||
>/dev/null 2>&1; do :; done
|
||
while ufw --force delete allow "${port}/${protocol}" >/dev/null 2>&1; do :; done
|
||
done < "$MANAGED_PORTS_CONFIG"
|
||
ufw reload >/dev/null 2>&1 || true
|
||
log "Управляемые правила UFW удалены"
|
||
}
|
||
|
||
restore_ipv6_defaults() {
|
||
rm -f -- "$IPV6_SYSCTL_FILE"
|
||
if command -v sysctl >/dev/null 2>&1; then
|
||
sysctl -w net.ipv6.conf.all.disable_ipv6=0 >/dev/null 2>&1 || true
|
||
sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null 2>&1 || true
|
||
fi
|
||
if [[ -f /etc/default/ufw && ! -L /etc/default/ufw ]]; then
|
||
if grep -q '^IPV6=' /etc/default/ufw; then
|
||
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
|
||
else
|
||
printf '\nIPV6=yes\n' >> /etc/default/ufw
|
||
fi
|
||
fi
|
||
command -v ufw >/dev/null 2>&1 && ufw reload >/dev/null 2>&1 || true
|
||
}
|
||
|
||
reset_ssh_and_user() {
|
||
local ssh_service="" configured_user="" login_user="${SUDO_USER:-}"
|
||
|
||
if [[ -f "$SETUP_ENV" && ! -L "$SETUP_ENV" ]]; then
|
||
configured_user="$(sed -n 's/^NEW_USERNAME=//p' "$SETUP_ENV" | head -n1 | tr -d '\r')"
|
||
fi
|
||
|
||
rm -f -- "$SSHD_DROPIN" "$FAIL2BAN_JAIL"
|
||
if command -v sshd >/dev/null 2>&1; then
|
||
sshd -t || die "После удаления drop-in конфигурация SSH не прошла проверку"
|
||
fi
|
||
for ssh_service in ssh sshd; do
|
||
if service_exists "$ssh_service"; then
|
||
systemctl restart "$ssh_service"
|
||
log "SSH перезапущен; проверьте новый вход через порт 22"
|
||
break
|
||
fi
|
||
done
|
||
service_exists fail2ban && systemctl restart fail2ban >/dev/null 2>&1 || true
|
||
|
||
if [[ -n "$configured_user" && "$configured_user" != root ]] && id "$configured_user" >/dev/null 2>&1; then
|
||
rm -f -- "/etc/sudoers.d/${configured_user}"
|
||
if ask_yes_no "Удалить созданного администратора ${configured_user} вместе с домашней папкой?" no; then
|
||
if [[ -n "$login_user" && "$configured_user" == "$login_user" ]]; then
|
||
warn "Нельзя удалить пользователя текущей sudo/SSH-сессии: ${configured_user}"
|
||
else
|
||
read -rp "Для удаления введите имя пользователя ${configured_user}: " confirmation
|
||
if [[ "$confirmation" == "$configured_user" ]]; then
|
||
userdel --remove "$configured_user"
|
||
log "Пользователь ${configured_user} удалён"
|
||
else
|
||
warn "Имя не совпало; пользователь оставлен"
|
||
fi
|
||
fi
|
||
else
|
||
warn "Пользователь ${configured_user} и его членство в группах оставлены"
|
||
fi
|
||
fi
|
||
|
||
rm -f -- "$SETUP_ENV" "$SETUP_HASH"
|
||
rmdir /root/setup-data >/dev/null 2>&1 || true
|
||
}
|
||
|
||
remove_telegram() {
|
||
if [[ -f "$PAM_SSHD" && ! -L "$PAM_SSHD" ]]; then
|
||
cp -a -- "$PAM_SSHD" "${PAM_SSHD}.bak.before-ochenstarik-uninstall.$(date +%F-%H%M%S)"
|
||
sed -i "\|${TELEGRAM_SCRIPT}|d;\|${TELEGRAM_LEGACY_SCRIPT}|d" "$PAM_SSHD"
|
||
fi
|
||
rm -f -- "$TELEGRAM_CONFIG" "$TELEGRAM_SCRIPT" "$TELEGRAM_LEGACY_SCRIPT" \
|
||
"$TELEGRAM_LOG" "$TELEGRAM_LOGROTATE"
|
||
find "$CONFIG_DIR" -maxdepth 1 -type f -name 'telegram.conf.bak.*' -delete 2>/dev/null || true
|
||
command -v sshd >/dev/null 2>&1 && sshd -t \
|
||
|| warn "Не удалось проверить SSH после удаления Telegram PAM-hook"
|
||
}
|
||
|
||
remove_xray() {
|
||
stop_disable_service ochenstarik-xray-rollback.timer
|
||
stop_disable_service ochenstarik-xray-rollback.service
|
||
stop_disable_service ochenstarik-xray-routing.service
|
||
[[ ! -x "$XRAY_HELPER" ]] || "$XRAY_HELPER" stop >/dev/null 2>&1 || true
|
||
if command -v nft >/dev/null 2>&1; then
|
||
nft list table ip ochenstarik_xray >/dev/null 2>&1 \
|
||
&& nft delete table ip ochenstarik_xray || true
|
||
nft list table ip6 ochenstarik_xray6 >/dev/null 2>&1 \
|
||
&& nft delete table ip6 ochenstarik_xray6 || true
|
||
fi
|
||
stop_disable_service xray.service
|
||
stop_disable_service xray@.service
|
||
rm -f -- "$XRAY_HELPER" "$XRAY_SERVICE" /etc/systemd/system/xray.service \
|
||
/etc/systemd/system/xray@.service /usr/local/bin/xray
|
||
rm -rf -- "$XRAY_STATE_DIR" "$XRAY_CONFIG_DIR" "$XRAY_SHARE_DIR" "$XRAY_LOG_DIR"
|
||
}
|
||
|
||
remove_panel_and_warp() {
|
||
stop_disable_service x-ui.service
|
||
if command -v warp-cli >/dev/null 2>&1; then
|
||
warp-cli --accept-tos disconnect >/dev/null 2>&1 || true
|
||
warp-cli --accept-tos registration delete >/dev/null 2>&1 || true
|
||
fi
|
||
stop_disable_service warp-svc.service
|
||
|
||
rm -f -- /etc/systemd/system/x-ui.service /usr/bin/x-ui /usr/local/bin/x-ui
|
||
rm -rf -- /usr/local/x-ui /etc/x-ui
|
||
|
||
if command -v apt-get >/dev/null 2>&1 && dpkg-query -W -f='${Status}' cloudflare-warp \
|
||
2>/dev/null | grep -Fqx 'install ok installed'; then
|
||
DEBIAN_FRONTEND=noninteractive apt-get purge -y cloudflare-warp
|
||
fi
|
||
rm -f -- /etc/apt/sources.list.d/cloudflare-client.list \
|
||
/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
|
||
}
|
||
|
||
read_backup_root() {
|
||
local value="/var/backups/ochenstarik-server" line key parsed_value
|
||
if [[ -f "$BACKUP_CONFIG" && ! -L "$BACKUP_CONFIG" ]]; then
|
||
[[ "$(stat -c '%u' "$BACKUP_CONFIG")" == 0 ]] \
|
||
|| die "Файл $BACKUP_CONFIG должен принадлежать root"
|
||
[[ "$(stat -c '%a' "$BACKUP_CONFIG")" =~ ^[0-7]*[0-5][0-5]$ ]] \
|
||
|| die "Файл $BACKUP_CONFIG не должен быть доступен для записи группе или всем"
|
||
parsed_value=""
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
line="${line//$'\r'/}"
|
||
[[ -n "$line" && "$line" != \#* ]] || continue
|
||
[[ "$line" == *=* ]] || die "Некорректная строка в $BACKUP_CONFIG: $line"
|
||
key="${line%%=*}"
|
||
value="${line#*=}"
|
||
case "$key" in
|
||
BACKUP_ROOT)
|
||
[[ "$value" =~ ^/[A-Za-z0-9._/-]+$ ]] \
|
||
|| die "Некорректный BACKUP_ROOT в $BACKUP_CONFIG"
|
||
parsed_value="$value"
|
||
;;
|
||
*) die "Неизвестный ключ в $BACKUP_CONFIG: $key" ;;
|
||
esac
|
||
done < "$BACKUP_CONFIG"
|
||
value="${parsed_value:-$value}"
|
||
fi
|
||
[[ "$value" == /* && "$value" != / ]] || die "Опасный путь резервных копий: $value"
|
||
case "$value" in
|
||
/var|/var/backups|/home|/root|/mnt|/media|/etc|/usr|/opt)
|
||
warn "Общий каталог ${value} не будет очищен автоматически"
|
||
printf ''
|
||
return 0
|
||
;;
|
||
esac
|
||
printf '%s' "$value"
|
||
}
|
||
|
||
remove_backup_automation() {
|
||
local schedule backup_root
|
||
for schedule in daily weekly monthly; do
|
||
stop_disable_service "ochenstarik-backup-${schedule}.timer"
|
||
done
|
||
rm -f -- /etc/systemd/system/ochenstarik-backup@.service \
|
||
/etc/systemd/system/ochenstarik-backup-daily.timer \
|
||
/etc/systemd/system/ochenstarik-backup-weekly.timer \
|
||
/etc/systemd/system/ochenstarik-backup-monthly.timer "$BACKUP_RUNNER"
|
||
|
||
backup_root="$(read_backup_root)"
|
||
if [[ -n "$backup_root" && -d "$backup_root" ]] \
|
||
&& ask_yes_no "Удалить все архивы из ${backup_root}?" no; then
|
||
read -rp 'Для необратимого удаления введите УДАЛИТЬ БЭКАПЫ: ' confirmation
|
||
if [[ "$confirmation" == 'УДАЛИТЬ БЭКАПЫ' ]]; then
|
||
if command -v chattr >/dev/null 2>&1; then
|
||
find "$backup_root" -xdev -type f -exec chattr -i -- {} + 2>/dev/null || true
|
||
fi
|
||
rm -rf -- "$backup_root/initial" "$backup_root/daily" \
|
||
"$backup_root/weekly" "$backup_root/monthly"
|
||
log "Архивы проекта удалены"
|
||
else
|
||
warn "Фраза не совпала; архивы оставлены"
|
||
fi
|
||
else
|
||
warn "Архивы оставлены; они не помешают повторной установке"
|
||
fi
|
||
rm -f -- "$BACKUP_CONFIG"
|
||
}
|
||
|
||
restore_locale() {
|
||
local package_name package_file
|
||
local -a packages=()
|
||
if [[ -f "$LOCALE_BACKUP" && ! -L "$LOCALE_BACKUP" ]]; then
|
||
cp -a -- "$LOCALE_BACKUP" /etc/default/locale
|
||
log "Предыдущая системная локаль восстановлена"
|
||
elif [[ -f "$LEGACY_LOCALE_BACKUP" && ! -L "$LEGACY_LOCALE_BACKUP" ]]; then
|
||
cp -a -- "$LEGACY_LOCALE_BACKUP" /etc/default/locale
|
||
log "Предыдущая системная локаль восстановлена"
|
||
elif [[ -f "$LOCALE_ABSENT_MARKER" && ! -L "$LOCALE_ABSENT_MARKER" ]]; then
|
||
rm -f -- /etc/default/locale
|
||
log "Созданный скриптом файл локали удалён"
|
||
elif [[ -f "$LEGACY_LOCALE_ABSENT_MARKER" \
|
||
&& ! -L "$LEGACY_LOCALE_ABSENT_MARKER" ]]; then
|
||
rm -f -- /etc/default/locale
|
||
log "Созданный скриптом файл локали удалён"
|
||
fi
|
||
|
||
for package_file in "$LOCALE_PACKAGES" "$LEGACY_LOCALE_PACKAGES"; do
|
||
[[ -f "$package_file" && ! -L "$package_file" ]] || continue
|
||
while IFS= read -r package_name || [[ -n "$package_name" ]]; do
|
||
[[ "$package_name" =~ ^[a-z0-9][a-z0-9+.-]*$ ]] \
|
||
&& [[ ! " ${packages[*]} " =~ " ${package_name} " ]] \
|
||
&& packages+=("$package_name")
|
||
done < "$package_file"
|
||
done
|
||
if ((${#packages[@]} > 0)) && ask_yes_no \
|
||
"Удалить языковые пакеты, которые ранее отсутствовали?" yes; then
|
||
DEBIAN_FRONTEND=noninteractive apt-get purge -y "${packages[@]}"
|
||
fi
|
||
rm -f -- "$LOCALE_BACKUP" "$LOCALE_ABSENT_MARKER" "$LOCALE_PACKAGES" \
|
||
"$LEGACY_LOCALE_BACKUP" "$LEGACY_LOCALE_ABSENT_MARKER" \
|
||
"$LEGACY_LOCALE_PACKAGES"
|
||
}
|
||
|
||
remove_step1_programs() {
|
||
local package_name
|
||
local -a packages=()
|
||
[[ -f "$PROGRAM_PACKAGES" && ! -L "$PROGRAM_PACKAGES" ]] || return 0
|
||
while IFS= read -r package_name || [[ -n "$package_name" ]]; do
|
||
[[ "$package_name" =~ ^[a-z0-9][a-z0-9+.-]*$ ]] && packages+=("$package_name")
|
||
done < "$PROGRAM_PACKAGES"
|
||
if ((${#packages[@]} > 0)) && ask_yes_no \
|
||
"Удалить дополнительные программы, установленные первым этапом?" yes; then
|
||
DEBIAN_FRONTEND=noninteractive apt-get purge -y "${packages[@]}"
|
||
fi
|
||
rm -f -- "$PROGRAM_PACKAGES"
|
||
}
|
||
|
||
remove_swap() {
|
||
[[ -e "$SWAP_SYSCTL_FILE" || -e "$SWAPFILE" ]] || return 0
|
||
ask_yes_no "Отключить и удалить ${SWAPFILE}?" yes || return 0
|
||
swapoff "$SWAPFILE" >/dev/null 2>&1 || true
|
||
if [[ -f /etc/fstab && ! -L /etc/fstab ]]; then
|
||
cp -a -- /etc/fstab "/etc/fstab.bak.before-ochenstarik-uninstall.$(date +%F-%H%M%S)"
|
||
sed -i '\|^[[:space:]]*/swapfile[[:space:]].*[[:space:]]swap[[:space:]]|d' /etc/fstab
|
||
fi
|
||
rm -f -- "$SWAP_SYSCTL_FILE"
|
||
if [[ -f "$SWAPFILE" && ! -L "$SWAPFILE" ]]; then
|
||
rm -f -- "$SWAPFILE"
|
||
else
|
||
warn "${SWAPFILE} не является обычным файлом и не удалён"
|
||
fi
|
||
sysctl --system >/dev/null 2>&1 || true
|
||
}
|
||
|
||
[[ "$EUID" -eq 0 ]] || die "Запустите скрипт от имени root"
|
||
|
||
cat <<'WARNING'
|
||
|
||
Этот скрипт удалит настройки, службы и данные, созданные проектом lightweight-server.
|
||
Он не откатывает обновления Ubuntu и не удаляет обязательные общие пакеты второго
|
||
этапа (OpenSSH, UFW и другие), потому что они нужны для доступа к серверу. Пакеты,
|
||
которые первый этап зафиксировал как новые, можно удалить отдельным подтверждением.
|
||
|
||
Сначала будет открыт SSH-порт 22. Не закрывайте текущую сессию, пока не проверите
|
||
новый вход после очистки.
|
||
WARNING
|
||
|
||
read -rp 'Для продолжения введите СБРОСИТЬ СЕРВЕР: ' final_confirmation
|
||
[[ "$final_confirmation" == 'СБРОСИТЬ СЕРВЕР' ]] || die "Очистка отменена"
|
||
|
||
remove_ufw_rules
|
||
restore_ipv6_defaults
|
||
reset_ssh_and_user
|
||
remove_telegram
|
||
remove_xray
|
||
remove_panel_and_warp
|
||
remove_backup_automation
|
||
restore_locale
|
||
remove_step1_programs
|
||
remove_swap
|
||
|
||
rm -f -- "$SSH_PORT_CONFIG" "$IP_FAMILY_CONFIG" "$MANAGED_PORTS_CONFIG"
|
||
find "$CONFIG_DIR" -maxdepth 1 -type f -name '*.bak.*' -delete 2>/dev/null || true
|
||
rmdir "$CONFIG_DIR" >/dev/null 2>&1 || true
|
||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||
|
||
log "Очистка компонентов lightweight-server завершена"
|
||
printf '\nПроверьте вход в новой сессии: ssh -p 22 root@IP_СЕРВЕРА\n'
|
||
printf 'После проверки можно снова запускать скрипты установки по порядку.\n'
|
||
if [[ -n "$(current_ssh_port)" && "$(current_ssh_port)" != 22 ]]; then
|
||
printf 'Правило текущего старого SSH-порта %s/tcp оставлено. Удалите его после проверки нового входа.\n' \
|
||
"$(current_ssh_port)"
|
||
fi
|