362 lines
12 KiB
Bash
Executable file
362 lines
12 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
readonly CONFIG_DIR="/etc/ochenstarik-server"
|
||
readonly SSH_PORT_CONFIG="${CONFIG_DIR}/ssh-port.conf"
|
||
readonly IP_FAMILY_CONFIG="${CONFIG_DIR}/ip-family.conf"
|
||
readonly MANAGED_PORTS_CONFIG="${CONFIG_DIR}/ufw-managed-ports.conf"
|
||
readonly IPV6_SYSCTL_FILE="/etc/sysctl.d/99-zz-ochenstarik-disable-ipv6.conf"
|
||
readonly UFW_DEFAULTS="/etc/default/ufw"
|
||
readonly DEFAULT_SSH_PORT="20202"
|
||
|
||
log() { printf '[+] %s\n' "$*"; }
|
||
warn() { printf '[!] %s\n' "$*" >&2; }
|
||
die() { printf '[x] %s\n' "$*" >&2; exit 1; }
|
||
|
||
require_command() {
|
||
command -v "$1" >/dev/null 2>&1 || die "Не найдена команда: $1"
|
||
}
|
||
|
||
is_valid_port() {
|
||
local port="$1"
|
||
[[ "$port" =~ ^[0-9]{1,5}$ ]] || return 1
|
||
((10#$port >= 1 && 10#$port <= 65535))
|
||
}
|
||
|
||
read_saved_value() {
|
||
local file="$1" key="$2"
|
||
[[ -e "$file" ]] || return 0
|
||
[[ -f "$file" && ! -L "$file" ]] \
|
||
|| die "$file должен быть обычным файлом, а не символической ссылкой"
|
||
sed -n "s/^${key}=//p" "$file" | head -n1 | tr -d '\r'
|
||
}
|
||
|
||
select_action() {
|
||
local choice default_choice=1
|
||
|
||
[[ ! -e "$IP_FAMILY_CONFIG" ]] || default_choice=2
|
||
printf 'Выберите действие:\n'
|
||
printf ' 1) Новая установка или полное обновление пакетов\n'
|
||
printf ' 2) Изменить режим IPv4/IPv6 без установки пакетов\n'
|
||
printf ' 3) Добавить открытые порты без установки пакетов\n'
|
||
while :; do
|
||
read -rp "Номер действия [${default_choice}]: " choice || die "Ввод был прерван"
|
||
choice="${choice:-$default_choice}"
|
||
case "$choice" in
|
||
1) ACTION=install; return 0 ;;
|
||
2) ACTION=ip-mode; return 0 ;;
|
||
3) ACTION=add-ports; return 0 ;;
|
||
*) warn "Введите 1, 2 или 3" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
choose_ssh_port() {
|
||
local default_port="$DEFAULT_SSH_PORT" saved_port selected_port
|
||
|
||
saved_port="$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)"
|
||
if [[ -n "$saved_port" ]]; then
|
||
if is_valid_port "$saved_port"; then
|
||
default_port="$((10#$saved_port))"
|
||
else
|
||
warn "Сохранённый SSH-порт некорректен: $saved_port"
|
||
fi
|
||
fi
|
||
|
||
while :; do
|
||
read -rp "SSH-порт для этапа 3 [${default_port}]: " selected_port \
|
||
|| die "Ввод был прерван"
|
||
selected_port="${selected_port:-$default_port}"
|
||
if is_valid_port "$selected_port"; then
|
||
SSH_PORT="$((10#$selected_port))"
|
||
return 0
|
||
fi
|
||
warn "SSH-порт должен быть числом от 1 до 65535"
|
||
done
|
||
}
|
||
|
||
choose_ip_mode() {
|
||
local saved_mode default_choice choice
|
||
|
||
saved_mode="$(read_saved_value "$IP_FAMILY_CONFIG" IP_MODE)"
|
||
case "$saved_mode" in
|
||
ipv4) default_choice=1 ;;
|
||
ipv6) default_choice=2 ;;
|
||
both) default_choice=3 ;;
|
||
*) default_choice=1 ;;
|
||
esac
|
||
|
||
printf '\nВыберите семейства IP для входящих подключений:\n'
|
||
printf ' 1) Только IPv4 — входящие IPv6-порты закрываются через UFW\n'
|
||
printf ' 2) Только IPv6 — открытые входящие порты доступны лишь по IPv6\n'
|
||
printf ' 3) IPv4 + IPv6\n'
|
||
while :; do
|
||
read -rp "Режим [${default_choice}]: " choice || die "Ввод был прерван"
|
||
choice="${choice:-$default_choice}"
|
||
case "$choice" in
|
||
1) IP_MODE=ipv4; return 0 ;;
|
||
2) IP_MODE=ipv6; return 0 ;;
|
||
3) IP_MODE=both; return 0 ;;
|
||
*) warn "Введите 1, 2 или 3" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
add_managed_rule() {
|
||
local rule="$1"
|
||
[[ -n "${RULE_SEEN[$rule]:-}" ]] && return 0
|
||
MANAGED_RULES+=("$rule")
|
||
RULE_SEEN["$rule"]=1
|
||
}
|
||
|
||
load_managed_rules() {
|
||
local rule port protocol
|
||
MANAGED_RULES=()
|
||
RULE_SEEN=()
|
||
[[ -e "$MANAGED_PORTS_CONFIG" ]] || return 0
|
||
[[ -f "$MANAGED_PORTS_CONFIG" && ! -L "$MANAGED_PORTS_CONFIG" ]] \
|
||
|| die "$MANAGED_PORTS_CONFIG должен быть обычным файлом"
|
||
|
||
while IFS= read -r rule || [[ -n "$rule" ]]; do
|
||
rule="${rule//$'\r'/}"
|
||
[[ -n "$rule" && "$rule" != \#* ]] || continue
|
||
[[ "$rule" =~ ^([0-9]+)/(tcp|udp)$ ]] \
|
||
|| die "Некорректное правило в $MANAGED_PORTS_CONFIG: $rule"
|
||
port="${BASH_REMATCH[1]}"
|
||
protocol="${BASH_REMATCH[2]}"
|
||
is_valid_port "$port" || die "Некорректный порт в $MANAGED_PORTS_CONFIG: $port"
|
||
add_managed_rule "$((10#$port))/${protocol}"
|
||
done < "$MANAGED_PORTS_CONFIG"
|
||
}
|
||
|
||
add_default_rules() {
|
||
local saved_ssh_port
|
||
|
||
saved_ssh_port="${SSH_PORT:-$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)}"
|
||
is_valid_port "$saved_ssh_port" || saved_ssh_port="$DEFAULT_SSH_PORT"
|
||
for rule in \
|
||
22/tcp "$((10#$saved_ssh_port))/tcp" 80/tcp 2096/tcp 443/tcp \
|
||
40000/tcp 63636/tcp; do
|
||
add_managed_rule "$rule"
|
||
done
|
||
}
|
||
|
||
collect_additional_ports() {
|
||
local input normalized token port protocol rule
|
||
local -a tokens additions=()
|
||
local -A additions_seen=()
|
||
|
||
printf '\nВведите порты через пробел или запятую. TCP используется по умолчанию.\n'
|
||
printf 'Пример: 80 443/tcp 53/udp 40000\n'
|
||
while :; do
|
||
read -rp 'Порты: ' input || die "Ввод был прерван"
|
||
normalized="${input//,/ }"
|
||
read -r -a tokens <<< "$normalized"
|
||
additions=()
|
||
additions_seen=()
|
||
((${#tokens[@]} > 0)) || { warn "Введите хотя бы один порт"; continue; }
|
||
|
||
for token in "${tokens[@]}"; do
|
||
if [[ ! "$token" =~ ^([0-9]+)(/(tcp|udp))?$ ]]; then
|
||
warn "Некорректное правило: $token"
|
||
additions=()
|
||
break
|
||
fi
|
||
port="${BASH_REMATCH[1]}"
|
||
protocol="${BASH_REMATCH[3]:-tcp}"
|
||
if ! is_valid_port "$port"; then
|
||
warn "Порт должен быть от 1 до 65535: $port"
|
||
additions=()
|
||
break
|
||
fi
|
||
rule="$((10#$port))/${protocol}"
|
||
if [[ -z "${additions_seen[$rule]:-}" ]]; then
|
||
additions+=("$rule")
|
||
additions_seen["$rule"]=1
|
||
fi
|
||
done
|
||
((${#additions[@]} > 0)) && break
|
||
done
|
||
|
||
for rule in "${additions[@]}"; do
|
||
add_managed_rule "$rule"
|
||
done
|
||
}
|
||
|
||
save_configuration() {
|
||
local config_file
|
||
install -d -m 700 -o root -g root "$CONFIG_DIR"
|
||
for config_file in "$SSH_PORT_CONFIG" "$IP_FAMILY_CONFIG" "$MANAGED_PORTS_CONFIG"; do
|
||
[[ ! -L "$config_file" ]] || die "Отказ от записи через символическую ссылку: $config_file"
|
||
done
|
||
printf 'SSH_PORT=%s\n' "$SSH_PORT" > "$SSH_PORT_CONFIG"
|
||
printf '%s\n' "${MANAGED_RULES[@]}" > "$MANAGED_PORTS_CONFIG"
|
||
chown root:root "$SSH_PORT_CONFIG" "$MANAGED_PORTS_CONFIG"
|
||
chmod 600 "$SSH_PORT_CONFIG" "$MANAGED_PORTS_CONFIG"
|
||
}
|
||
|
||
set_ufw_ipv6_setting() {
|
||
local value="$1"
|
||
[[ -f "$UFW_DEFAULTS" && ! -L "$UFW_DEFAULTS" ]] \
|
||
|| die "Не найден безопасный файл $UFW_DEFAULTS"
|
||
if grep -q '^IPV6=' "$UFW_DEFAULTS"; then
|
||
sed -i "s/^IPV6=.*/IPV6=${value}/" "$UFW_DEFAULTS"
|
||
else
|
||
printf '\nIPV6=%s\n' "$value" >> "$UFW_DEFAULTS"
|
||
fi
|
||
}
|
||
|
||
enable_ipv6_systemwide() {
|
||
local sysctl_path
|
||
local -a sysctl_paths=(/proc/sys/net/ipv6/conf/*/disable_ipv6)
|
||
|
||
rm -f -- "$IPV6_SYSCTL_FILE"
|
||
for sysctl_path in "${sysctl_paths[@]}"; do
|
||
[[ -e "$sysctl_path" ]] || continue
|
||
printf '0\n' > "$sysctl_path" || die "Не удалось включить IPv6 через $sysctl_path"
|
||
done
|
||
}
|
||
|
||
restore_ipv6_stack_if_managed() {
|
||
[[ -e "$IPV6_SYSCTL_FILE" ]] || return 0
|
||
enable_ipv6_systemwide
|
||
}
|
||
|
||
check_session_safety() {
|
||
local client_ip client_port server_ip server_port
|
||
|
||
[[ -n "${SSH_CONNECTION:-}" ]] || return 0
|
||
read -r client_ip client_port server_ip server_port <<< "$SSH_CONNECTION"
|
||
if [[ "$IP_MODE" == ipv4 && "$server_ip" == *:* ]]; then
|
||
die "Текущая SSH-сессия использует IPv6. Сначала подключитесь по IPv4 или используйте консоль провайдера"
|
||
fi
|
||
if [[ "$IP_MODE" == ipv6 && "$server_ip" == *.* ]]; then
|
||
die "Текущая SSH-сессия использует IPv4. Для режима IPv6 подключитесь по IPv6 или используйте консоль провайдера"
|
||
fi
|
||
}
|
||
|
||
verify_ipv6_connectivity() {
|
||
ip -6 address show scope global | grep -q 'inet6' \
|
||
|| die "На сервере нет глобального IPv6-адреса"
|
||
ip -6 route show default | grep -q '^default' \
|
||
|| die "На сервере нет маршрута IPv6 по умолчанию"
|
||
}
|
||
|
||
delete_managed_ufw_rules() {
|
||
local rule port protocol attempt
|
||
|
||
for rule in "${MANAGED_RULES[@]}"; do
|
||
port="${rule%/*}"
|
||
protocol="${rule#*/}"
|
||
for attempt in {1..5}; do
|
||
ufw --force delete allow "$rule" >/dev/null 2>&1 || break
|
||
done
|
||
ufw --force delete allow from 0.0.0.0/0 to any port "$port" proto "$protocol" \
|
||
>/dev/null 2>&1 || true
|
||
ufw --force delete allow from ::/0 to any port "$port" proto "$protocol" \
|
||
>/dev/null 2>&1 || true
|
||
done
|
||
}
|
||
|
||
allow_rule_for_selected_families() {
|
||
local rule="$1" port protocol
|
||
port="${rule%/*}"
|
||
protocol="${rule#*/}"
|
||
|
||
if [[ "$IP_MODE" == ipv4 || "$IP_MODE" == both ]]; then
|
||
ufw allow from 0.0.0.0/0 to any port "$port" proto "$protocol"
|
||
fi
|
||
if [[ "$IP_MODE" == ipv6 || "$IP_MODE" == both ]]; then
|
||
ufw allow from ::/0 to any port "$port" proto "$protocol"
|
||
fi
|
||
}
|
||
|
||
apply_ip_mode_and_firewall() {
|
||
local rule
|
||
|
||
check_session_safety
|
||
if [[ "$IP_MODE" == ipv4 ]]; then
|
||
restore_ipv6_stack_if_managed
|
||
set_ufw_ipv6_setting yes
|
||
delete_managed_ufw_rules
|
||
else
|
||
enable_ipv6_systemwide
|
||
[[ "$IP_MODE" != ipv6 ]] || verify_ipv6_connectivity
|
||
set_ufw_ipv6_setting yes
|
||
delete_managed_ufw_rules
|
||
fi
|
||
|
||
ufw default deny incoming
|
||
ufw default allow outgoing
|
||
for rule in "${MANAGED_RULES[@]}"; do
|
||
log "Открываю ${rule} для режима ${IP_MODE}"
|
||
allow_rule_for_selected_families "$rule"
|
||
done
|
||
ufw --force enable
|
||
ufw reload >/dev/null 2>&1 || true
|
||
[[ ! -L "$IP_FAMILY_CONFIG" ]] \
|
||
|| die "Отказ от записи через символическую ссылку: $IP_FAMILY_CONFIG"
|
||
printf 'IP_MODE=%s\n' "$IP_MODE" > "$IP_FAMILY_CONFIG"
|
||
chown root:root "$IP_FAMILY_CONFIG"
|
||
chmod 600 "$IP_FAMILY_CONFIG"
|
||
}
|
||
|
||
[[ "$EUID" -eq 0 ]] || die "Запустите этот скрипт от имени root"
|
||
declare -a MANAGED_RULES=()
|
||
declare -A RULE_SEEN=()
|
||
ACTION=""
|
||
SSH_PORT=""
|
||
IP_MODE=""
|
||
select_action
|
||
|
||
if [[ "$ACTION" == install ]]; then
|
||
choose_ssh_port
|
||
choose_ip_mode
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
|
||
log "Обновляю операционную систему"
|
||
apt-get update
|
||
apt-get upgrade -y
|
||
|
||
log "Устанавливаю обязательные пакеты SSH, брандмауэра и защиты"
|
||
apt-get install -y \
|
||
sudo ufw fail2ban curl ca-certificates openssl openssh-server logrotate iproute2
|
||
|
||
for command_name in ip sed sysctl ufw; do
|
||
require_command "$command_name"
|
||
done
|
||
install -d -m 700 -o root -g root "$CONFIG_DIR"
|
||
load_managed_rules
|
||
add_default_rules
|
||
save_configuration
|
||
apply_ip_mode_and_firewall
|
||
log "SSH-порт ${SSH_PORT} сохранён для этапа 3"
|
||
else
|
||
for command_name in ip sed sysctl ufw; do
|
||
require_command "$command_name"
|
||
done
|
||
install -d -m 700 -o root -g root "$CONFIG_DIR"
|
||
SSH_PORT="$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)"
|
||
is_valid_port "$SSH_PORT" || SSH_PORT="$DEFAULT_SSH_PORT"
|
||
load_managed_rules
|
||
add_default_rules
|
||
|
||
if [[ "$ACTION" == ip-mode ]]; then
|
||
choose_ip_mode
|
||
else
|
||
IP_MODE="$(read_saved_value "$IP_FAMILY_CONFIG" IP_MODE)"
|
||
case "$IP_MODE" in ipv4|ipv6|both) ;; *) choose_ip_mode ;; esac
|
||
collect_additional_ports
|
||
fi
|
||
save_configuration
|
||
apply_ip_mode_and_firewall
|
||
fi
|
||
|
||
log "Текущая конфигурация"
|
||
printf 'Режим IP: %s\n' "$IP_MODE"
|
||
printf 'IPv6 в ядре: %s\n' "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null || printf 'недоступно')"
|
||
ufw status verbose
|
||
|
||
printf '\nГотово. Следующий этап: ochenstarik-server-user-3.sh\n'
|
||
printf 'Этап 3 перенесёт SSH на порт %s.\n' "$SSH_PORT"
|
||
printf 'Не удаляйте правило порта 22, пока не проверите вход по порту %s.\n' "$SSH_PORT"
|