mirror of
https://github.com/bivlked/amneziawg-installer.git
synced 2026-10-19 05:03:18 +00:00
The diagnostic report exists to be pasted into a public issue: the bug report
template asks for its contents. It masked exactly one line, `PrivateKey = ...`,
and printed the rest of the server config verbatim. `PresharedKey` lives in that
same file, written there whenever a client is created with `--psk`, so a user who
had enabled the extra layer on top of AWG 2.0 published the preshared key of
every peer, destroying the one thing that layer was there for.
Key values are now stripped by one shared function applied at two points: at the
report boundary and separately to the server config, so that detaching the outer
pipeline cannot silently uncover the riskiest raw input.
Four contexts are covered, each for a reason found by reading upstream
amneziawg-tools rather than guessed:
* configuration lines, anchored, with an optional comment marker, masked to end
of line. Config parsing strips whitespace before parsing (config.c,
config_read_line), so `PrivateKey = AA BB=` is a valid record and cutting at
the first space would have left the tail of the key in the report.
* `awg show` labels. That command hides the private and preshared keys but
prints the header protection key with key(), in clear text (show.c).
* `Line unrecognized: ...`, which echoes the whole line to stderr and therefore
into the journal that the report prints as a section.
* `Key is not the correct length or format: ...`, which carries no key name at
all and cannot be matched by one.
Matching is case insensitive because config parsing is (get_value uses
strncasecmp), and anchored because an unanchored version damaged unrelated
fields: the server name is free text and may itself contain `PrivateKey = ...`.
Public keys, client names and obfuscation parameters are deliberately left in
place: without them the report cannot be used to correlate peers, which is what
it is read for. The report header and the issue template now name both what is
stripped and what is kept; previously both listed only IP addresses, ports and
routes, which invited the reader to assume nothing else was in there.
Verified on a live server: with the pre-fix installer the preshared key value is
present in the report, with this one it is not, while public keys, client names
and the server name survive. A search across all issues, pull requests and
discussions of this repository found no exposed key value.
tests/test_diagnostic_report_secrets.bats covers eleven spellings and the
negative controls; with the fix reverted all thirteen checks fail.
4367 lines
252 KiB
Bash
4367 lines
252 KiB
Bash
#!/bin/bash
|
||
|
||
# Проверка минимальной версии Bash
|
||
if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then
|
||
echo "ОШИБКА: Требуется Bash >= 4.0 (текущая: ${BASH_VERSION})" >&2; exit 1
|
||
fi
|
||
|
||
# ==============================================================================
|
||
# Скрипт для установки и настройки AmneziaWG 2.0 на Ubuntu/Debian серверах
|
||
# Автор: @bivlked
|
||
# Версия: 5.27.1
|
||
# Дата: 2026-08-22
|
||
# Репозиторий: https://github.com/bivlked/amneziawg-installer
|
||
# ==============================================================================
|
||
|
||
# --- Безопасный режим и Константы ---
|
||
set -o pipefail
|
||
|
||
SCRIPT_VERSION="5.27.1"
|
||
AWG_DIR="/root/awg"
|
||
CONFIG_FILE="$AWG_DIR/awgsetup_cfg.init"
|
||
STATE_FILE="$AWG_DIR/setup_state"
|
||
LOG_FILE="$AWG_DIR/install_amneziawg.log"
|
||
KEYS_DIR="$AWG_DIR/keys"
|
||
SERVER_CONF_FILE="/etc/amnezia/amneziawg/awg0.conf"
|
||
AWG_BRANCH="${AWG_BRANCH:-v${SCRIPT_VERSION}}"
|
||
COMMON_SCRIPT_URL="https://raw.githubusercontent.com/bivlked/amneziawg-installer/${AWG_BRANCH}/awg_common.sh"
|
||
COMMON_SCRIPT_PATH="$AWG_DIR/awg_common.sh"
|
||
MANAGE_SCRIPT_URL="https://raw.githubusercontent.com/bivlked/amneziawg-installer/${AWG_BRANCH}/manage_amneziawg.sh"
|
||
MANAGE_SCRIPT_PATH="$AWG_DIR/manage_amneziawg.sh"
|
||
|
||
# SHA256 checksums скачиваемых скриптов. Обновляются при каждом релизе.
|
||
# Проверяются в step5_download_scripts() после curl.
|
||
# Если AWG_BRANCH переопределён (не v$SCRIPT_VERSION), проверка пропускается.
|
||
# Формат: sha256sum output (hex, 64 chars).
|
||
COMMON_SCRIPT_SHA256="b5a8355c17e0836550c3a3a4aab73b052e3c439ac440d409741ba0134df98449"
|
||
MANAGE_SCRIPT_SHA256="a06d856bf496b8b7ea1cba6c2f503400fdff895b830ea9e357d8752d121933c5"
|
||
|
||
# AmneziaWG 2.0 пин (H0, 31 jul 2026). Upstream влил AmneziaWG 3.0 в default-ветку
|
||
# amneziawg-linux-kernel-module, и PPA переключился на 3.0. Тогда на ядрах старее
|
||
# 6.7 (Debian 12 = 6.1) DKMS-сборка из PPA падала на nla_put_uint. 31 jul вечером
|
||
# upstream это исправил (v3.0.20260731-04; проверено нами на Debian 12 / 6.1.0-51
|
||
# 1 aug), поэтому пин из ВЫНУЖДЕННОГО стал СОЗНАТЕЛЬНЫМ: на старых ядрах держим
|
||
# проверенный 2.0-модуль, пока 3.0 не валидирован отдельно. AWG2_PIN_COMMIT
|
||
# сверяется после clone (integrity: надёжнее хрупкого tarball-SHA - тег
|
||
# теоретически можно переместить, immutable-коммит нельзя).
|
||
AWG2_PIN_TAG="v1.0.20260725"
|
||
AWG2_PIN_COMMIT="ae0924ca700520ca34c5bdbcfd05b2f683ea9353"
|
||
|
||
# Флаги CLI
|
||
UNINSTALL=0; HELP=0; HELP_EXIT_RC=0; DIAGNOSTIC=0; VERBOSE=0; NO_COLOR=0; AUTO_YES=0; NO_TWEAKS=0; NO_CPS=0; KEEP_PACKAGES=""
|
||
FORCE_REINSTALL=0
|
||
_APT_UPDATED=0
|
||
CLI_PORT=""; CLI_SUBNET=""; CLI_DISABLE_IPV6="default"; CLI_SSH_PORT=""
|
||
CLI_ROUTING_MODE="default"; CLI_CUSTOM_ROUTES=""; CLI_ENDPOINT=""; CLI_NO_TWEAKS=0; CLI_NO_CPS=0; CLI_KEEP_PACKAGES=0
|
||
CLI_ALLOW_IPV6_TUNNEL=0
|
||
CLI_ISOLATION="default"
|
||
CLI_SERVER_NAME=""
|
||
CLI_MOBILE=0
|
||
|
||
# --- Автоочистка временных файлов ---
|
||
_install_temp_files=()
|
||
_install_cleaned=0
|
||
_install_cleanup() {
|
||
# Идемпотентно: на INT/TERM зовётся из сигнального обработчика, затем ещё раз
|
||
# на EXIT - второй вызов должен быть no-op.
|
||
[[ "$_install_cleaned" -eq 1 ]] && return 0
|
||
_install_cleaned=1
|
||
local f
|
||
for f in "${_install_temp_files[@]}"; do [[ -f "$f" ]] && rm -f "$f"; done
|
||
# Очистка временных файлов из awg_common.sh (если уже подключён через source)
|
||
type _awg_cleanup &>/dev/null && _awg_cleanup
|
||
}
|
||
# На INT/TERM раньше cleanup срабатывал, но скрипт НЕ завершался - выполнение
|
||
# продолжалось после прерванной команды (опасно посреди apt/dpkg/правки конфигов),
|
||
# и cleanup ещё раз шёл на EXIT. Теперь сигнал = cleanup + явный выход 130/143.
|
||
_install_on_signal() {
|
||
_install_cleanup
|
||
exit "$1"
|
||
}
|
||
trap _install_cleanup EXIT
|
||
trap '_install_on_signal 130' INT
|
||
trap '_install_on_signal 143' TERM
|
||
|
||
# --- Обработка аргументов ---
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--uninstall) UNINSTALL=1 ;;
|
||
--help|-h) HELP=1 ;;
|
||
--diagnostic) DIAGNOSTIC=1 ;;
|
||
--verbose|-v) VERBOSE=1 ;;
|
||
--no-color) NO_COLOR=1 ;;
|
||
--port=*) CLI_PORT="${1#*=}" ;;
|
||
--ssh-port=*) CLI_SSH_PORT="${1#*=}" ;;
|
||
--subnet=*) CLI_SUBNET="${1#*=}" ;;
|
||
--allow-ipv6) CLI_DISABLE_IPV6=0 ;;
|
||
--disallow-ipv6) CLI_DISABLE_IPV6=1 ;;
|
||
--allow-ipv6-tunnel) CLI_ALLOW_IPV6_TUNNEL=1 ;;
|
||
--route-all) CLI_ROUTING_MODE=1 ;;
|
||
--route-amnezia) CLI_ROUTING_MODE=2 ;;
|
||
--route-custom=*) CLI_ROUTING_MODE=3; CLI_CUSTOM_ROUTES="${1#*=}" ;;
|
||
--isolation=*) CLI_ISOLATION="${1#*=}" ;;
|
||
--endpoint=*) CLI_ENDPOINT="${1#*=}" ;;
|
||
--server-name=*) CLI_SERVER_NAME="${1#*=}" ;;
|
||
--mobile) CLI_MOBILE=1 ;;
|
||
--yes|-y) AUTO_YES=1 ;;
|
||
--no-tweaks) NO_TWEAKS=1; CLI_NO_TWEAKS=1 ;;
|
||
--no-cps) NO_CPS=1; CLI_NO_CPS=1 ;;
|
||
--keep-packages) KEEP_PACKAGES=1; CLI_KEEP_PACKAGES=1 ;;
|
||
--force|-f) FORCE_REINSTALL=1 ;;
|
||
--preset=*) CLI_PRESET="${1#*=}" ;;
|
||
--jc=*) CLI_JC="${1#*=}" ;;
|
||
--jmin=*) CLI_JMIN="${1#*=}" ;;
|
||
--jmax=*) CLI_JMAX="${1#*=}" ;;
|
||
*) echo "Неизвестный аргумент: $1" >&2; HELP=1; HELP_EXIT_RC=1 ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
# ==============================================================================
|
||
# Функции логирования
|
||
# ==============================================================================
|
||
|
||
log_msg() {
|
||
local type="$1" msg="$2"
|
||
local ts
|
||
ts=$(date +'%F %T')
|
||
local entry="[$ts] $type: $msg"
|
||
local color_start="" color_end=""
|
||
|
||
if [[ "$NO_COLOR" -eq 0 ]]; then
|
||
color_end="\033[0m"
|
||
case "$type" in
|
||
INFO) color_start="\033[0;32m" ;;
|
||
WARN) color_start="\033[0;33m" ;;
|
||
ERROR) color_start="\033[1;31m" ;;
|
||
DEBUG) color_start="\033[0;36m" ;;
|
||
*) color_start=""; color_end="" ;;
|
||
esac
|
||
fi
|
||
|
||
if ! mkdir -p "$(dirname "$LOG_FILE")" || ! echo "$entry" >> "$LOG_FILE"; then
|
||
echo "[$ts] ERROR: Ошибка записи лога $LOG_FILE" >&2
|
||
fi
|
||
|
||
if [[ "$type" == "ERROR" || "$type" == "WARN" ]]; then
|
||
printf "${color_start}%s${color_end}\n" "$entry" >&2
|
||
elif [[ "$type" == "DEBUG" && "$VERBOSE" -eq 1 ]]; then
|
||
printf "${color_start}%s${color_end}\n" "$entry" >&2
|
||
elif [[ "$type" == "INFO" ]]; then
|
||
printf "${color_start}%s${color_end}\n" "$entry"
|
||
elif [[ "$type" != "DEBUG" ]]; then
|
||
printf "${color_start}%s${color_end}\n" "$entry"
|
||
fi
|
||
}
|
||
|
||
log() { log_msg "INFO" "$1"; }
|
||
log_warn() { log_msg "WARN" "$1"; }
|
||
log_error() { log_msg "ERROR" "$1"; }
|
||
log_debug() { if [[ "$VERBOSE" -eq 1 ]]; then log_msg "DEBUG" "$1"; fi; }
|
||
die() { log_error "КРИТИЧЕСКАЯ ОШИБКА: $1"; log_error "Установка прервана. Лог: $LOG_FILE"; exit 1; }
|
||
|
||
# ==============================================================================
|
||
# apt-get update wrapper, игнорирующий 404 только на source packages (deb-src).
|
||
# INLINE: нужна в шагах 1-2 до скачивания awg_common.sh (Step 5).
|
||
# Некоторые зеркала (Hetzner, AWS) не раздают source, но дефолтный ubuntu.sources
|
||
# содержит 'Types: deb deb-src'. Source не нужен (DKMS + бинарные headers).
|
||
# Возвращает 0 если update прошёл ИЛИ если все ошибки — только на source-маркерах.
|
||
# Любая другая ошибка (GPG, сетевая на binary, silent crash/OOM/SIGKILL) → non-zero.
|
||
# ==============================================================================
|
||
apt_update_tolerant() {
|
||
# --ppa-amnezia-tolerant: дополнительно игнорируем ошибки от PPA Amnezia.
|
||
# Используется на step 2 — там apt_wait_for_ppa_package сам делает retry
|
||
# для outage'а ppa.launchpadcontent.net (issue #68). Без этого флага мы
|
||
# должны fall-fail на любых non-source ошибках, иначе скрипт продолжит
|
||
# установку на stale apt-cache (PR #69 review finding).
|
||
local ppa_tolerant=0
|
||
if [[ "${1:-}" == "--ppa-amnezia-tolerant" ]]; then
|
||
ppa_tolerant=1
|
||
shift
|
||
fi
|
||
|
||
local err_output rc non_src_errors raw_had_non_src_errors=0
|
||
err_output=$(LANG=C LC_ALL=C apt-get update -y 2>&1)
|
||
rc=$?
|
||
echo "$err_output"
|
||
|
||
if [[ $rc -eq 0 ]]; then
|
||
return 0
|
||
fi
|
||
|
||
# Фильтруем строки ошибок. Игнорируем:
|
||
# 1. Строки про source-пакеты (deb-src / /source/ / Sources)
|
||
# 2. Generic 'Some index files failed to download' — симптом, не причина
|
||
# Дополнительно исключаем заведомо информационные W:-строки, которые не
|
||
# бывают ПРИЧИНОЙ rc!=0, но переживали фильтры и превращали tolerable-сбой
|
||
# (например deb-src 404 при задвоенных sources) в ложный fatal:
|
||
# - "Target ... is configured multiple times" (дубль sources-записей)
|
||
# - "... stored in legacy trusted.gpg keyring" (старый формат ключей)
|
||
non_src_errors=$(printf '%s\n' "$err_output" \
|
||
| grep -E '^(E:|Err:|W:)' \
|
||
| grep -vE '(deb-src|/source/|Sources([^[:alpha:]]|$))' \
|
||
| grep -vE 'Some index files failed to download' \
|
||
| grep -vE '^W: (Target .* is configured multiple times|.* stored in legacy trusted\.gpg)' || true)
|
||
|
||
# Запоминаем pre-PPA filter состояние: нужно различать «были реальные APT-ошибки,
|
||
# но все на PPA Amnezia» (tolerant OK) от «классифицируемых ошибок не было
|
||
# вообще» (OOM / silent crash — НЕ tolerant даже если в выводе мелькает PPA URL).
|
||
[[ -n "$non_src_errors" ]] && raw_had_non_src_errors=1
|
||
|
||
# Опционально (step 2): убираем ошибки только на PPA Amnezia — они будут
|
||
# повторно проверены через apt_wait_for_ppa_package по apt-cache (issue #68).
|
||
if [[ $ppa_tolerant -eq 1 && -n "$non_src_errors" ]]; then
|
||
non_src_errors=$(printf '%s\n' "$non_src_errors" \
|
||
| grep -vE 'ppa\.launchpadcontent\.net.*amnezia' || true)
|
||
fi
|
||
|
||
if [[ -z "$non_src_errors" ]]; then
|
||
# Граничный случай: rc != 0, но ни одной классифицируемой строки E:/Err:/W:
|
||
# не найдено (SIGKILL от OOM, silent crash, неизвестный формат вывода apt).
|
||
# Игнорировать можно ТОЛЬКО если в выводе есть явные source-маркеры,
|
||
# либо ppa-tolerant + были реальные APT-строки и все они — на PPA Amnezia.
|
||
if printf '%s\n' "$err_output" | grep -qE '(deb-src|/source/|Sources([^[:alpha:]]|$))'; then
|
||
log_warn "apt update: source packages недоступны в зеркале (ожидаемо, игнорируется)"
|
||
return 0
|
||
fi
|
||
if [[ $ppa_tolerant -eq 1 && $raw_had_non_src_errors -eq 1 ]] \
|
||
&& printf '%s\n' "$err_output" | grep -qE 'ppa\.launchpadcontent\.net.*amnezia'; then
|
||
log_warn "apt update: ошибки только на PPA Amnezia (issue #68), продолжаем с retry."
|
||
return 0
|
||
fi
|
||
log_error "apt update завершился с rc=$rc без классифицируемых APT-строк — возможен silent crash / OOM / SIGKILL"
|
||
return "$rc"
|
||
fi
|
||
|
||
log_error "apt update завершился с non-source ошибками:"
|
||
printf '%s\n' "$non_src_errors" | while IFS= read -r line; do
|
||
log_error " $line"
|
||
done
|
||
return "$rc"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# apt_wait_for_ppa_package <package> [max_attempts] [initial_delay_seconds]
|
||
# Ждёт, пока пакет станет видимым в apt-cache, с экспоненциальным
|
||
# backoff между попытками. Нужно на шаге 2 после добавления PPA
|
||
# Amnezia: ppa.launchpadcontent.net иногда коротко лежит (issue #68),
|
||
# и без ретрая первая холодная установка валится, хотя через минуту
|
||
# PPA уже доступен.
|
||
#
|
||
# ВАЖНО: проверяется именно apt-cache show, а не rc от apt-get update.
|
||
# apt-get update toлerantно возвращает 0 даже когда какой-то InRelease
|
||
# не скачался — поэтому простого retry на rc недостаточно для outage
|
||
# PPA. Видимость пакета в apt-cache — единственный надёжный сигнал,
|
||
# что PPA реально проиндексировался.
|
||
#
|
||
# С дефолтами (3 попытки × initial=30с) сценарий такой: попытка 1 →
|
||
# sleep 30с → apt update + попытка 2 → sleep 60с → apt update +
|
||
# попытка 3 (последняя). После третьего fail возвращаем 1.
|
||
# Итого ожидание между попытками ≈1.5 минуты.
|
||
#
|
||
# Cap на delay (1800с) защищает от арифметического переполнения, если
|
||
# кто-то вызовет helper с очень большим max.
|
||
# ==============================================================================
|
||
apt_wait_for_ppa_package() {
|
||
local pkg="$1" max="${2:-3}" delay="${3:-30}" attempt
|
||
for ((attempt = 1; attempt <= max; attempt++)); do
|
||
if apt-cache show "$pkg" >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
if (( attempt == max )); then
|
||
return 1
|
||
fi
|
||
log_warn "Пакет '${pkg}' не появился в apt-cache (попытка ${attempt}/${max}, PPA пока недоступен), повтор через ${delay}с..."
|
||
sleep "$delay"
|
||
apt_update_tolerant >/dev/null 2>&1 || true
|
||
delay=$(( delay * 2 > 1800 ? 1800 : delay * 2 ))
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Справка
|
||
# ==============================================================================
|
||
|
||
show_help() {
|
||
cat << 'EOF'
|
||
Использование: sudo bash install_amneziawg.sh [ОПЦИИ]
|
||
Скрипт для установки и настройки AmneziaWG 2.0 на Ubuntu (24.04 / 25.10 / 26.04) и Debian (12 / 13).
|
||
|
||
Опции:
|
||
-h, --help Показать эту справку и выйти
|
||
--uninstall Удалить AmneziaWG и все его конфигурации
|
||
--diagnostic Создать диагностический отчет и выйти
|
||
-v, --verbose Расширенный вывод для отладки (включая DEBUG)
|
||
--no-color Отключить цветной вывод в терминале
|
||
--port=НОМЕР Установить UDP порт (1-65535) неинтерактивно
|
||
--ssh-port=ПОРТ SSH-порт для правила UFW (по умолчанию определяется
|
||
автоматически; список через запятую). Используйте, если
|
||
SSH на нестандартном порту и автодетект недоступен
|
||
--subnet=ПОДСЕТЬ Подсеть туннеля, CIDR /16-/30 (напр. 10.9.0.0/16) неинтерактивно
|
||
--allow-ipv6 Оставить IPv6 включенным неинтерактивно
|
||
--disallow-ipv6 Принудительно отключить IPv6 неинтерактивно
|
||
--allow-ipv6-tunnel Включить dual-stack IPv6 внутри туннеля (ULA, opt-in)
|
||
--route-all Использовать режим 'Весь трафик' неинтерактивно
|
||
--route-amnezia Использовать режим 'Amnezia' неинтерактивно
|
||
--route-custom=СЕТИ Использовать режим 'Пользовательский' неинтерактивно
|
||
--isolation=on|off Изоляция клиентов VPN друг от друга (по умолчанию on).
|
||
off: подсеть туннеля добавляется в AllowedIPs клиентов
|
||
--endpoint=АДРЕС Внешний endpoint сервера: FQDN, IPv4 или [IPv6] (для NAT)
|
||
--server-name=ИМЯ Имя сервера в приложении Amnezia при импорте vpn://
|
||
(по умолчанию 'AWG Server'; без кавычек и управляющих символов)
|
||
--mobile Мобильный сетап одним флагом: --preset=mobile + порт 443/udp
|
||
(мобильные операторы часто глушат нестандартные UDP-порты).
|
||
Явный --port=N выигрывает над портом 443
|
||
-y, --yes Автоматическое подтверждение (перезагрузки, UFW и т.д.)
|
||
-f, --force Принудительная переустановка поверх уже работающего AmneziaWG
|
||
(по умолчанию запуск на сконфигурированном сервере прерывается;
|
||
ENV: AWG_FORCE_REINSTALL=1 эквивалентен флагу)
|
||
--no-tweaks Пропустить очистку системы, оптимизацию и hardening (UFW,
|
||
Fail2Ban); минимальный forwarding-sysctl применяется всегда
|
||
--keep-packages Не удалять системные пакеты (snapd и др.), но оставить
|
||
фаервол, Fail2Ban и оптимизацию. Снос snapd уносит
|
||
установленные снапы и их данные в /var/snap
|
||
--preset=ТИП Набор параметров обфускации: default, mobile
|
||
mobile: Jc=3, узкий Jmax — для мобильных операторов (Tele2, Yota, Megafon)
|
||
--jc=N Задать Jc вручную (1-128, поверх preset)
|
||
--jmin=N Задать Jmin вручную (0-1280, поверх preset)
|
||
--jmax=N Задать Jmax вручную (0-1280, поверх preset, должно быть >= Jmin)
|
||
--no-cps Отключить CPS (параметр I1) - нужно, если десктопный
|
||
AmneziaVPN на macOS виснет при подключении (issue #159)
|
||
|
||
Примеры:
|
||
sudo bash install_amneziawg.sh # Интерактивная установка
|
||
sudo bash install_amneziawg.sh --port=51820 --route-all # Неинтерактивная
|
||
sudo bash install_amneziawg.sh --route-amnezia --yes # Полностью автоматическая
|
||
sudo bash install_amneziawg.sh --preset=mobile --yes # Оптимизация для мобильных сетей
|
||
sudo bash install_amneziawg.sh --uninstall # Удаление
|
||
sudo bash install_amneziawg.sh --diagnostic # Диагностика
|
||
|
||
Репозиторий: https://github.com/bivlked/amneziawg-installer
|
||
EOF
|
||
# Явный --help завершается с 0; неизвестный аргумент - с 1 (ложный успех в CI).
|
||
exit "${HELP_EXIT_RC:-0}"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Утилиты и валидация
|
||
# ==============================================================================
|
||
|
||
update_state() {
|
||
local next_step=$1
|
||
mkdir -p "$(dirname "$STATE_FILE")"
|
||
# Атомарная запись: tmp-файл + flock + mv. Защита от битого
|
||
# состояния при crash/power-loss между write и close.
|
||
(
|
||
flock -x 200
|
||
local tmp="${STATE_FILE}.tmp.$BASHPID"
|
||
if printf '%s\n' "$next_step" > "$tmp" && mv -f "$tmp" "$STATE_FILE"; then
|
||
exit 0
|
||
fi
|
||
rm -f "$tmp" 2>/dev/null
|
||
exit 1
|
||
) 200>"${STATE_FILE}.lock" || die "Ошибка записи состояния"
|
||
log "Состояние: следующий шаг - $next_step"
|
||
}
|
||
|
||
request_reboot() {
|
||
local next_step=$1
|
||
update_state "$next_step"
|
||
|
||
# Перед reboot-gate 1→2 сохраняем boot_id. На входе step 2
|
||
# сравниваем с текущим — если совпадает, reboot не произошёл
|
||
# и DKMS соберёт модуль под старое ядро (которое после следующего
|
||
# reboot будет уже apt full-upgrade'нутым и не подхватит модуль).
|
||
if [[ "$next_step" == "2" ]] && [[ -r /proc/sys/kernel/random/boot_id ]]; then
|
||
if cat /proc/sys/kernel/random/boot_id > "$AWG_DIR/.boot_id_before_step2" 2>/dev/null; then
|
||
log_debug "boot_id captured before reboot"
|
||
fi
|
||
fi
|
||
|
||
echo "" >> "$LOG_FILE"
|
||
log_warn "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||
log_warn "!!! ТРЕБУЕТСЯ ПЕРЕЗАГРУЗКА СИСТЕМЫ !!!"
|
||
log_warn "!!! После перезагрузки, запустите скрипт снова командой: !!!"
|
||
log_warn "!!! sudo bash $0 [с теми же параметрами, если были] !!!"
|
||
log_warn "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||
echo "" >> "$LOG_FILE"
|
||
local confirm="y"
|
||
if [[ "$AUTO_YES" -eq 0 ]]; then
|
||
read -rp "Перезагрузить сейчас? [y/N]: " confirm < /dev/tty
|
||
else
|
||
log "Автоматическое подтверждение перезагрузки (--yes)."
|
||
fi
|
||
if [[ "$confirm" =~ ^[[:space:]]*[Yy]([Ee][Ss])?[[:space:]]*$ ]]; then
|
||
log "Инициирована перезагрузка..."
|
||
sleep 5
|
||
if ! reboot; then die "Команда reboot не удалась."; fi
|
||
exit 1
|
||
else
|
||
log "Перезагрузка отменена. Перезагрузитесь вручную и запустите скрипт снова."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Ранний детект контейнера (LXC/OpenVZ/Docker/WSL) - кейс с 4pda: на
|
||
# контейнерном VDS установка доходила до шага 3 и падала сырым 'modprobe:
|
||
# FATAL: Module amneziawg not found' без объяснения причины. AmneziaWG ставит
|
||
# модуль ядра через DKMS, а контейнеры разделяют ядро с хостом и не дают
|
||
# загружать свои модули - честнее остановиться сразу и объяснить.
|
||
# systemd-detect-virt есть на всех поддерживаемых Ubuntu/Debian; если его
|
||
# вдруг нет - проверку пропускаем (мягкая деградация, установку не блокируем).
|
||
check_container() {
|
||
command -v systemd-detect-virt &>/dev/null || return 0
|
||
local virt
|
||
virt=$(systemd-detect-virt --container 2>/dev/null) || true
|
||
[[ -z "$virt" || "$virt" == "none" ]] && return 0
|
||
log_error "Обнаружен контейнер: ${virt}."
|
||
log_error "AmneziaWG требует загрузки модуля ядра (DKMS), а контейнеры (LXC/OpenVZ/Docker/WSL) разделяют ядро с хостом и не позволяют загружать свои модули."
|
||
die "Возьмите полноценный VPS (KVM/QEMU) или bare-metal. Вариант для контейнеров - userspace amneziawg-go: ADVANCED.md, раздел 'LXC / Docker через amneziawg-go'."
|
||
}
|
||
|
||
check_os_version() {
|
||
log "Проверка ОС..."
|
||
|
||
# Определение через /etc/os-release (универсально для Ubuntu и Debian)
|
||
OS_ID=""
|
||
OS_VERSION=""
|
||
OS_CODENAME=""
|
||
if [[ -f /etc/os-release ]]; then
|
||
# shellcheck source=/dev/null
|
||
source /etc/os-release
|
||
OS_ID="$ID"
|
||
OS_VERSION="$VERSION_ID"
|
||
OS_CODENAME="$VERSION_CODENAME"
|
||
elif command -v lsb_release &>/dev/null; then
|
||
OS_ID=$(lsb_release -si | tr '[:upper:]' '[:lower:]')
|
||
OS_VERSION=$(lsb_release -sr)
|
||
OS_CODENAME=$(lsb_release -sc)
|
||
else
|
||
log_warn "Не удалось определить ОС (/etc/os-release и lsb_release не найдены)."
|
||
return 0
|
||
fi
|
||
export OS_ID OS_VERSION OS_CODENAME
|
||
|
||
# Поддерживаемые ОС
|
||
local supported=0
|
||
case "$OS_ID" in
|
||
ubuntu)
|
||
if [[ "$OS_VERSION" == "24.04" || "$OS_VERSION" == "25.10" || "$OS_VERSION" == "26.04" ]]; then
|
||
supported=1
|
||
fi
|
||
;;
|
||
debian)
|
||
if [[ "$OS_VERSION" == "12" || "$OS_VERSION" == "13" ]]; then
|
||
supported=1
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
if [[ "$supported" -eq 1 ]]; then
|
||
log "ОС: ${OS_ID^} $OS_VERSION ($OS_CODENAME) — поддерживается"
|
||
else
|
||
log_warn "Обнаружена $OS_ID $OS_VERSION ($OS_CODENAME). Скрипт протестирован на Ubuntu 24.04/25.10/26.04 и Debian 12/13."
|
||
if [[ "$AUTO_YES" -eq 0 ]]; then
|
||
read -rp "Продолжить? [y/N]: " confirm < /dev/tty
|
||
if ! [[ "$confirm" =~ ^[[:space:]]*[Yy]([Ee][Ss])?[[:space:]]*$ ]]; then die "Отмена."; fi
|
||
else
|
||
log "Продолжаем на $OS_ID $OS_VERSION (--yes)."
|
||
fi
|
||
fi
|
||
}
|
||
|
||
check_kernel_version() {
|
||
# Модуль AmneziaWG 2.0 собирается через DKMS против ядра хоста. На ядрах
|
||
# старше 5.15 (Ubuntu < 22.04, напр. 5.4 на 20.04) сборка обычно падает уже
|
||
# на шаге 2 - невнятным package-failure. Предупреждаем ЯВНО и рано, до
|
||
# обновлений и перезагрузок (issue #163). Не die: на части старых ядер модуль
|
||
# всё же собирается (HWE и подобное), поэтому WARN + подтверждение.
|
||
local kver kmaj kmin
|
||
kver=$(uname -r)
|
||
if [[ "$kver" =~ ^([0-9]+)\.([0-9]+) ]]; then
|
||
kmaj=${BASH_REMATCH[1]}; kmin=${BASH_REMATCH[2]}
|
||
else
|
||
log_warn "Не удалось разобрать версию ядра ('$kver') - пропускаю проверку минимальной версии."
|
||
return 0
|
||
fi
|
||
if (( kmaj < 5 || (kmaj == 5 && kmin < 15) )); then
|
||
log_warn "Ядро $kver старее 5.15 - для модуля AmneziaWG 2.0 это обычно слишком старо."
|
||
log_warn "DKMS-сборка модуля на таком ядре чаще всего падает. Рекомендуется переустановить VPS на Ubuntu 24.04 LTS или Debian 12 (либо новее). Матрица: Ubuntu 24.04/25.10/26.04, Debian 12/13."
|
||
if [[ "$AUTO_YES" -eq 0 ]]; then
|
||
read -rp "Всё равно продолжить? [y/N]: " confirm < /dev/tty
|
||
if ! [[ "$confirm" =~ ^[[:space:]]*[Yy]([Ee][Ss])?[[:space:]]*$ ]]; then die "Отмена: ядро $kver слишком старое для модуля AmneziaWG 2.0."; fi
|
||
else
|
||
log "Продолжаем на ядре $kver (--yes)."
|
||
fi
|
||
else
|
||
log "Ядро $kver (OK для модуля AmneziaWG 2.0)."
|
||
fi
|
||
}
|
||
|
||
# shellcheck disable=SC2120 # в установщике зовётся без аргументов (берёт uname -r); bats передаёт версии
|
||
_kernel_supports_awg3() {
|
||
# Возвращает 0, если версия ядра >= 6.7 - там модуль берём из PPA как есть.
|
||
# Возвращает 1, если ядро старее 6.7 - там идём пиновым 2.0-путём.
|
||
# ⚠️ Имя историческое, читать его буквально НЕЛЬЗЯ. Порог 6.7 появился 30-31 jul
|
||
# 2026: 3.0-код звал nla_put_uint, которой до mainline v6.7 не было, и на 6.1
|
||
# (Debian 12) сборка падала с 'implicit declaration of nla_put_uint'. 31 jul
|
||
# upstream это починил (v3.0.20260731-04), и на 6.1 3.0-модуль СОБИРАЕТСЯ -
|
||
# проверено на стенде 1 aug. Порог оставлен сознательно: линия 3.0 за сутки
|
||
# успела сломать и починить сборку именно на старых ядрах, то есть там она
|
||
# обкатана меньше всего, а пиновый 2.0 сверяется по immutable-коммиту. Снимать
|
||
# порог после отдельной валидации 3.0, а не потому, что сборка снова проходит.
|
||
# Arg $1: kernel release (default uname -r). Неразбираемую версию считаем
|
||
# "НЕ поддерживает" -> пиновый 2.0 (он собирается на ЛЮБОМ нашем ядре, так что
|
||
# консервативный выбор не ломает связь, лишь не даёт 3.0-фич, которых в H0 нет).
|
||
# Чистая функция без внешних зависимостей (bats: извлекается sed-range + source).
|
||
local kver="${1:-$(uname -r)}" kmaj kmin
|
||
local min_maj=6 min_min=7
|
||
if [[ "$kver" =~ ^([0-9]+)\.([0-9]+) ]]; then
|
||
kmaj=${BASH_REMATCH[1]}; kmin=${BASH_REMATCH[2]}
|
||
else
|
||
return 1
|
||
fi
|
||
if (( kmaj > min_maj || (kmaj == min_maj && kmin >= min_min) )); then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
check_free_space() {
|
||
log "Проверка места..."
|
||
local req=2048
|
||
local avail
|
||
avail=$(df -m / | awk 'NR==2 {print $4}')
|
||
if [[ -z "$avail" ]]; then
|
||
log_warn "Не удалось определить свободное место."
|
||
return 0
|
||
fi
|
||
if [ "$avail" -lt "$req" ]; then
|
||
log_warn "Доступно $avail МБ. Рекомендуется >= $req МБ."
|
||
if [[ "$AUTO_YES" -eq 0 ]]; then
|
||
read -rp "Продолжить? [y/N]: " confirm < /dev/tty
|
||
if ! [[ "$confirm" =~ ^[[:space:]]*[Yy]([Ee][Ss])?[[:space:]]*$ ]]; then die "Отмена."; fi
|
||
else
|
||
log "Продолжаем с $avail МБ (--yes)."
|
||
fi
|
||
else
|
||
log "Свободно: $avail МБ (OK)"
|
||
fi
|
||
}
|
||
|
||
check_port_availability() {
|
||
local port=$1
|
||
log "Проверка порта $port..."
|
||
local proc
|
||
proc=$(ss -lunp | grep ":${port} ")
|
||
if [[ -n "$proc" ]]; then
|
||
log_error "Порт ${port}/udp уже используется! Процесс: $proc"
|
||
return 1
|
||
else
|
||
log "Порт $port/udp свободен."
|
||
return 0
|
||
fi
|
||
}
|
||
|
||
install_packages() {
|
||
local packages=("$@")
|
||
local to_install=()
|
||
local pkg
|
||
log "Проверка пакетов: ${packages[*]}..."
|
||
for pkg in "${packages[@]}"; do
|
||
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then
|
||
to_install+=("$pkg")
|
||
fi
|
||
done
|
||
if [ ${#to_install[@]} -eq 0 ]; then
|
||
log "Все пакеты уже установлены."
|
||
return 0
|
||
fi
|
||
log "Установка: ${to_install[*]}..."
|
||
if [[ "${_APT_UPDATED:-0}" -eq 0 ]]; then
|
||
# C4: жёсткая ошибка apt_update_tolerant (GPG / сеть на binary-репо / OOM) -
|
||
# это НЕ source-шум, а реальный сбой; продолжать на устаревшем кэше нельзя
|
||
# (контракт стр.138, как у вызовов 1975/2108). die завершает установку, так
|
||
# что _APT_UPDATED=1 проставляется только при успехе - иначе повторный
|
||
# install_packages в этой сессии молча пропустил бы update.
|
||
apt_update_tolerant || die "Ошибка apt update."
|
||
_APT_UPDATED=1
|
||
fi
|
||
if ! DEBIAN_FRONTEND=noninteractive apt install -y "${to_install[@]}"; then
|
||
# v5.13.0: типичный сбой на 25.10/26.04 после in-place upgrade с 24.04 —
|
||
# dpkg postinst пакета amneziawg-dkms запускает `dkms autoinstall`,
|
||
# который итерируется по ВСЕМ ядрам в /lib/modules/. Старые 6.8.x
|
||
# headers скомпилированы gcc-13, а в 25.10 по умолчанию только
|
||
# gcc-15 → autoinstall фолится, dpkg не configure'ит зависящие
|
||
# amneziawg-tools / amneziawg. Принудительно собираем модуль для
|
||
# running ядра и завершаем dpkg --configure -a.
|
||
if printf '%s\n' "${to_install[@]}" | grep -qx "amneziawg-dkms"; then
|
||
log_warn "apt install не завершился — пробую DKMS-сборку только для текущего ядра $(uname -r)..."
|
||
local _mver
|
||
_mver="$(ls /var/lib/dkms/amneziawg/ 2>/dev/null | head -n1)"
|
||
if [[ -n "$_mver" ]] \
|
||
&& dkms install -m amneziawg -v "$_mver" -k "$(uname -r)" --force \
|
||
&& DEBIAN_FRONTEND=noninteractive dpkg --configure -a; then
|
||
log "DKMS-модуль собран для $(uname -r), dpkg сконфигурирован."
|
||
log "Пакеты установлены."
|
||
return 0
|
||
fi
|
||
fi
|
||
die "Ошибка установки пакетов."
|
||
fi
|
||
log "Пакеты установлены."
|
||
}
|
||
|
||
cleanup_apt() {
|
||
log "Очистка apt..."
|
||
apt-get clean || log_warn "Ошибка apt-get clean"
|
||
rm -rf /var/lib/apt/lists/* || log_warn "Ошибка rm /var/lib/apt/lists/*"
|
||
log "Кэш apt очищен."
|
||
}
|
||
|
||
configure_ipv6() {
|
||
if [[ "$CLI_DISABLE_IPV6" != "default" ]]; then
|
||
DISABLE_IPV6=$CLI_DISABLE_IPV6
|
||
log "IPv6 из CLI: $DISABLE_IPV6"
|
||
elif [[ "$AUTO_YES" -eq 1 ]]; then
|
||
DISABLE_IPV6=1
|
||
log "IPv6 отключен (--yes, по умолчанию)."
|
||
else
|
||
read -rp "Отключить IPv6 (рекомендуется)? [Y/n]: " dis_ipv6 < /dev/tty
|
||
if [[ "$dis_ipv6" =~ ^[Nn]$ ]]; then
|
||
DISABLE_IPV6=0
|
||
else
|
||
DISABLE_IPV6=1
|
||
fi
|
||
fi
|
||
export DISABLE_IPV6
|
||
log "Отключение IPv6: $(if [ "$DISABLE_IPV6" -eq 1 ]; then echo 'Да'; else echo 'Нет'; fi)"
|
||
}
|
||
|
||
# Определение наличия native IPv6 на VPS.
|
||
# Native IPv6 = глобально маршрутизируемый адрес (НЕ ULA fc00::/7, НЕ link-local
|
||
# fe80::) И наличие IPv6 default-маршрута. Любого из условий по отдельности мало:
|
||
# - global-адрес без default route -> нет выхода в IPv6-интернет (клиент с ::/0
|
||
# получит black-hole);
|
||
# - ULA (fddd::/...) для `ip` имеет scope global, но в интернет не маршрутизируется.
|
||
# Эхо 1 только когда выполнены оба условия, иначе 0.
|
||
detect_native_ipv6() {
|
||
local have_addr=0 have_route=0
|
||
if ip -6 addr show scope global 2>/dev/null \
|
||
| grep -oP 'inet6\s+\K[0-9a-fA-F:]+' \
|
||
| grep -qviE '^(fc|fd)'; then
|
||
have_addr=1
|
||
fi
|
||
if ip -6 route show default 2>/dev/null | grep -q .; then
|
||
have_route=1
|
||
fi
|
||
if [[ "$have_addr" -eq 1 && "$have_route" -eq 1 ]]; then
|
||
echo 1
|
||
else
|
||
echo 0
|
||
fi
|
||
}
|
||
|
||
configure_ipv6_tunnel() {
|
||
if [[ "$CLI_ALLOW_IPV6_TUNNEL" -eq 1 ]]; then
|
||
ALLOW_IPV6_TUNNEL=1
|
||
elif [[ -z "${ALLOW_IPV6_TUNNEL:-}" ]]; then
|
||
ALLOW_IPV6_TUNNEL=0
|
||
fi
|
||
: "${IPV6_SUBNET:=fddd:2c4:2c4:2c4::/64}"
|
||
# IPv6-туннель требует включённого IPv6 на хосте. Снимаю --disallow-ipv6 И
|
||
# активно включаю IPv6 в рантайме ДО detection/render: при upgrade с дефолтной
|
||
# прошлой установки (IPv6 был выключен в рантайме) ядро скрывает все IPv6-адреса,
|
||
# поэтому detect_native_ipv6 дал бы false-negative, а клиент отрендерился бы с
|
||
# IPv6 Address при выключенном в ядре IPv6 (awg-quick restart может упасть). weaq P1.
|
||
if [[ "$ALLOW_IPV6_TUNNEL" -eq 1 ]]; then
|
||
if [[ "$DISABLE_IPV6" -eq 1 ]]; then
|
||
log_warn "--allow-ipv6-tunnel requires host IPv6 forwarding; overriding --disallow-ipv6 (DISABLE_IPV6=0)"
|
||
DISABLE_IPV6=0
|
||
fi
|
||
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
|
||
sysctl -w net.ipv6.conf.lo.disable_ipv6=0 >/dev/null 2>&1 || true
|
||
fi
|
||
# Native IPv6 определяю ПОСЛЕ runtime-включения (кэширую в init для client render Phase 4).
|
||
SERVER_HAS_NATIVE_IPV6=$(detect_native_ipv6)
|
||
if [[ "$ALLOW_IPV6_TUNNEL" -eq 1 && "$SERVER_HAS_NATIVE_IPV6" -eq 0 ]]; then
|
||
log_warn "Native IPv6 не обнаружен на VPS - туннель IPv6 будет работать peer-to-peer без выхода в IPv6-интернет."
|
||
fi
|
||
export ALLOW_IPV6_TUNNEL IPV6_SUBNET SERVER_HAS_NATIVE_IPV6 DISABLE_IPV6
|
||
}
|
||
|
||
# Безопасная загрузка конфигурации (whitelist-парсер, без source/eval)
|
||
safe_load_config() {
|
||
local config_file="${1:-$CONFIG_FILE}"
|
||
if [[ ! -f "$config_file" ]]; then return 1; fi
|
||
|
||
local line key value first_line=1
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
if [[ "$first_line" -eq 1 ]]; then
|
||
line="${line#$'\xEF\xBB\xBF'}"
|
||
first_line=0
|
||
fi
|
||
line="${line%$'\r'}"
|
||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||
[[ -z "${line// /}" ]] && continue
|
||
line="${line#export }"
|
||
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
|
||
key="${BASH_REMATCH[1]}"
|
||
value="${BASH_REMATCH[2]}"
|
||
if [[ "$value" == \'*\' ]]; then
|
||
value="${value#\'}"
|
||
value="${value%\'}"
|
||
elif [[ "$value" == \"*\" ]]; then
|
||
value="${value#\"}"
|
||
value="${value%\"}"
|
||
fi
|
||
case "$key" in
|
||
OS_ID|OS_VERSION|OS_CODENAME|AWG_PORT|AWG_TUNNEL_SUBNET|\
|
||
DISABLE_IPV6|ALLOWED_IPS_MODE|ALLOWED_IPS|AWG_ENDPOINT|AWG_MTU|\
|
||
AWG_Jc|AWG_Jmin|AWG_Jmax|AWG_S1|AWG_S2|AWG_S3|AWG_S4|\
|
||
AWG_H1|AWG_H2|AWG_H3|AWG_H4|AWG_I1|AWG_I2|AWG_I3|AWG_I4|AWG_I5|AWG_PRESET|NO_TWEAKS|NO_CPS|KEEP_PACKAGES|\
|
||
AWG_APPLY_MODE|ALLOW_IPV6_TUNNEL|IPV6_SUBNET|SERVER_HAS_NATIVE_IPV6|PREV_AWG_PORT|CLIENT_ISOLATION|CLIENT_ISOLATION_NET|AWG_SERVER_NAME)
|
||
export "$key=$value"
|
||
;;
|
||
esac
|
||
fi
|
||
done < "$config_file"
|
||
}
|
||
|
||
# Чтение одного ключа из конфига (для точечных запросов)
|
||
safe_read_config_key() {
|
||
local key="$1" config_file="${2:-$CONFIG_FILE}"
|
||
local line first_line=1
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
if [[ "$first_line" -eq 1 ]]; then
|
||
line="${line#$'\xEF\xBB\xBF'}"
|
||
first_line=0
|
||
fi
|
||
line="${line%$'\r'}"
|
||
line="${line#export }"
|
||
if [[ "$line" =~ ^${key}=(.*)$ ]]; then
|
||
local value="${BASH_REMATCH[1]}"
|
||
if [[ "$value" == \'*\' ]]; then
|
||
value="${value#\'}"
|
||
value="${value%\'}"
|
||
elif [[ "$value" == \"*\" ]]; then
|
||
value="${value#\"}"
|
||
value="${value%\"}"
|
||
fi
|
||
echo "$value"
|
||
return 0
|
||
fi
|
||
done < "$config_file"
|
||
return 1
|
||
}
|
||
|
||
validate_jc_value() {
|
||
local v="$1"
|
||
[[ "$v" =~ ^[0-9]+$ ]] && [[ "$v" -ge 1 ]] && [[ "$v" -le 128 ]]
|
||
}
|
||
|
||
validate_junk_size() {
|
||
local v="$1"
|
||
[[ "$v" =~ ^[0-9]+$ ]] && [[ "$v" -ge 0 ]] && [[ "$v" -le 1280 ]]
|
||
}
|
||
|
||
validate_port() {
|
||
local port="$1"
|
||
# ^[1-9][0-9]{0,4}$ запрещает ведущие нули ('0080' иначе трактуется как octal
|
||
# в арифметике и проскакивает проверку диапазона) и ограничивает длину: без
|
||
# лимита 64-битная арифметика (( )) переполняется и 2^64+51820 проходил бы
|
||
# range-check. Сравнение - на чистом decimal.
|
||
if ! [[ "$port" =~ ^[1-9][0-9]{0,4}$ ]] || (( port > 65535 )); then
|
||
die "Некорректный порт: '$port'. Допустимый диапазон: 1-65535."
|
||
fi
|
||
}
|
||
|
||
validate_subnet() {
|
||
local subnet="$1" o
|
||
# Самодостаточно (шаг 0, ДО загрузки awg_common.sh): не используем _valid_ipv4/
|
||
# _cidr_bounds. Октеты без ведущих нулей ('010...' иначе трактуется как octal).
|
||
if ! [[ "$subnet" =~ ^(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})/([0-9]{1,2})$ ]]; then
|
||
die "Некорректная подсеть: '$subnet'. Ожидается CIDR /16-/30, напр. 10.9.0.0/16."
|
||
fi
|
||
local a="${BASH_REMATCH[1]}" b="${BASH_REMATCH[2]}" c="${BASH_REMATCH[3]}" d="${BASH_REMATCH[4]}" prefix="${BASH_REMATCH[5]}"
|
||
for o in "$a" "$b" "$c" "$d"; do
|
||
(( 10#$o <= 255 )) || die "Некорректная подсеть: '$subnet'. Октет вне диапазона 0-255."
|
||
done
|
||
(( 10#$prefix >= 16 && 10#$prefix <= 30 )) || die "Некорректная подсеть: '$subnet'. Поддерживается только маска /16-/30."
|
||
# Инлайн-арифметика: адрес обязан быть network или network+1.
|
||
local ip=$(( (10#$a << 24) | (10#$b << 16) | (10#$c << 8) | 10#$d ))
|
||
local mask=$(( (0xFFFFFFFF << (32 - 10#$prefix)) & 0xFFFFFFFF ))
|
||
local network=$(( ip & mask ))
|
||
local n1=$(( network + 1 ))
|
||
local srv="$(( (n1 >> 24) & 255 )).$(( (n1 >> 16) & 255 )).$(( (n1 >> 8) & 255 )).$(( n1 & 255 ))"
|
||
if (( ip != network && ip != n1 )); then
|
||
die "Некорректная подсеть: '$subnet'. Адрес сервера должен быть ${srv} (network+1) или укажите сеть."
|
||
fi
|
||
# Нормализация глобала к <network+1>/<prefix> (сервер = network+1).
|
||
AWG_TUNNEL_SUBNET="${srv}/${prefix}"
|
||
}
|
||
|
||
# Сеть туннеля из CIDR-строки (<network+1>/<prefix> -> <network>/<prefix>).
|
||
# Нужен изоляции клиентов (issue #178): при отключённой изоляции в AllowedIPs
|
||
# клиентов уходит именно network-адрес. Самодостаточно (шаг 0, ДО загрузки
|
||
# awg_common.sh): не используем _cidr_bounds/_int_to_ipv4.
|
||
tunnel_network_cidr() {
|
||
local subnet="${1:-$AWG_TUNNEL_SUBNET}"
|
||
if ! [[ "$subnet" =~ ^(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})/([0-9]{1,2})$ ]]; then
|
||
return 1
|
||
fi
|
||
local a="${BASH_REMATCH[1]}" b="${BASH_REMATCH[2]}" c="${BASH_REMATCH[3]}" d="${BASH_REMATCH[4]}" prefix="${BASH_REMATCH[5]}"
|
||
(( 10#$prefix <= 32 )) || return 1
|
||
local o
|
||
for o in "$a" "$b" "$c" "$d"; do (( 10#$o <= 255 )) || return 1; done
|
||
local ip=$(( (10#$a << 24) | (10#$b << 16) | (10#$c << 8) | 10#$d ))
|
||
local mask
|
||
if (( 10#$prefix == 0 )); then mask=0; else mask=$(( (0xFFFFFFFF << (32 - 10#$prefix)) & 0xFFFFFFFF )); fi
|
||
local net=$(( ip & mask ))
|
||
echo "$(( (net >> 24) & 255 )).$(( (net >> 16) & 255 )).$(( (net >> 8) & 255 )).$(( net & 255 ))/${prefix}"
|
||
}
|
||
|
||
# Явный выбор изоляции клиентов (issue #178). Приоритет:
|
||
# CLI-флаг > сохранённый конфиг > интерактивный вопрос (только первый запуск,
|
||
# без --yes) > 1 (изолированно). Старый конфиг без ключа = 1: до этой фичи
|
||
# split-режимы были изолированы де-факто, поведение сохраняется.
|
||
configure_client_isolation() {
|
||
case "$CLI_ISOLATION" in
|
||
on) CLIENT_ISOLATION=1; log "Изоляция клиентов из CLI: включена." ;;
|
||
off) CLIENT_ISOLATION=0; log "Изоляция клиентов из CLI: отключена." ;;
|
||
default)
|
||
if [[ -n "${CLIENT_ISOLATION:-}" ]]; then
|
||
log "Изоляция клиентов (из конфига): $( [[ "$CLIENT_ISOLATION" -eq 1 ]] && echo включена || echo отключена )."
|
||
elif [[ "${config_exists:-0}" -eq 1 ]]; then
|
||
CLIENT_ISOLATION=1
|
||
log "Изоляция клиентов: включена (конфиг до v5.20 - прежнее поведение)."
|
||
elif [[ "$AUTO_YES" -eq 1 ]]; then
|
||
CLIENT_ISOLATION=1
|
||
log "Изоляция клиентов: включена (--yes, по умолчанию)."
|
||
else
|
||
local r_iso
|
||
read -rp "Изолировать клиентов VPN друг от друга? [Y/n]: " r_iso < /dev/tty
|
||
case "$r_iso" in
|
||
[nN]*) CLIENT_ISOLATION=0; log "Изоляция клиентов отключена: клиенты будут видеть друг друга внутри VPN." ;;
|
||
*) CLIENT_ISOLATION=1; log "Изоляция клиентов включена." ;;
|
||
esac
|
||
fi
|
||
;;
|
||
*) die "Некорректное значение --isolation='$CLI_ISOLATION'. Допустимо: on|off." ;;
|
||
esac
|
||
export CLIENT_ISOLATION
|
||
}
|
||
|
||
# Приводит ALLOWED_IPS в соответствие с CLIENT_ISOLATION (идемпотентно,
|
||
# вызывается на каждом запуске после определения режима маршрутизации).
|
||
# Изоляция ВЫКЛ: сеть туннеля дописывается в список (режимы 2/3; в режиме 1
|
||
# 0.0.0.0/0 уже покрывает её). Изоляция ВКЛ: наш токен убирается из режима 2
|
||
# (round-trip off->on); режим 3 не трогаем - кастомный список принадлежит
|
||
# пользователю, а изоляцию всё равно обеспечивает DROP-правило на сервере.
|
||
# CLIENT_ISOLATION_NET хранит ownership нашего токена (пуст, если токен
|
||
# пользовательский или изоляция включена) - нужен, чтобы вычистить прежний
|
||
# маршрут при смене подсети туннеля (issue #178, финальный аудит).
|
||
_apply_isolation_to_allowed_ips() {
|
||
local net
|
||
net=$(tunnel_network_cidr "$AWG_TUNNEL_SUBNET") || return 0
|
||
# Убираем ВЕСЬ whitespace, не только пробелы: validate_cidr_list принимает
|
||
# табы как разделители, и токен с табом иначе не распознавался бы pattern-
|
||
# матчем ниже - задваивание вместо no-op (ревью PR #179).
|
||
local compact=",${ALLOWED_IPS//[[:space:]]/},"
|
||
|
||
# Смена подсети туннеля: наш прежний токен (persisted CLIENT_ISOLATION_NET)
|
||
# отличается от текущей сети - убираем в любом режиме и при любом состоянии
|
||
# изоляции: токен по конструкции добавлен нами, а не пользователем.
|
||
if [[ -n "${CLIENT_ISOLATION_NET:-}" && "$CLIENT_ISOLATION_NET" != "$net" ]]; then
|
||
if [[ "$compact" == *",${CLIENT_ISOLATION_NET},"* ]]; then
|
||
# Цикл, а не одиночный replace: в испорченном списке токен может
|
||
# встречаться несколько раз - вычищаем все копии (ревью PR #179).
|
||
while [[ "$compact" == *",${CLIENT_ISOLATION_NET},"* ]]; do
|
||
compact="${compact/,${CLIENT_ISOLATION_NET},/,}"
|
||
done
|
||
compact="${compact#,}"; compact="${compact%,}"
|
||
ALLOWED_IPS="${compact//,/, }"
|
||
log "Смена подсети туннеля: прежний маршрут ${CLIENT_ISOLATION_NET} убран из AllowedIPs клиентов."
|
||
compact=",${ALLOWED_IPS// /},"
|
||
fi
|
||
CLIENT_ISOLATION_NET=""
|
||
fi
|
||
|
||
if [[ "${CLIENT_ISOLATION:-1}" -eq 0 ]]; then
|
||
if [[ "$ALLOWED_IPS_MODE" == "1" ]]; then
|
||
CLIENT_ISOLATION_NET=""
|
||
elif [[ "$compact" == *",${net},"* ]]; then
|
||
# Уже есть: наш прежний (CLIENT_ISOLATION_NET==net сохранён) или
|
||
# пользовательский (CLIENT_ISOLATION_NET пуст) - ownership не меняем.
|
||
:
|
||
else
|
||
ALLOWED_IPS="${ALLOWED_IPS}, ${net}"
|
||
CLIENT_ISOLATION_NET="$net"
|
||
log "Изоляция отключена: подсеть туннеля ${net} добавлена в AllowedIPs клиентов."
|
||
fi
|
||
else
|
||
# Изоляция ВКЛ: режим 2 - токен убираем всегда (список генерируется нами);
|
||
# режим 3 - только если токен добавили мы (ownership в CLIENT_ISOLATION_NET).
|
||
if [[ "$compact" == *",${net},"* ]] \
|
||
&& { [[ "$ALLOWED_IPS_MODE" == "2" ]] || [[ "${CLIENT_ISOLATION_NET:-}" == "$net" ]]; }; then
|
||
while [[ "$compact" == *",${net},"* ]]; do
|
||
compact="${compact/,${net},/,}"
|
||
done
|
||
compact="${compact#,}"; compact="${compact%,}"
|
||
ALLOWED_IPS="${compact//,/, }"
|
||
log "Изоляция включена: подсеть туннеля ${net} убрана из AllowedIPs клиентов."
|
||
fi
|
||
CLIENT_ISOLATION_NET=""
|
||
fi
|
||
export CLIENT_ISOLATION_NET
|
||
}
|
||
|
||
# Валидация имени сервера для vpn:// URI (D#180): поле description показывается
|
||
# в приложении Amnezia после импорта. Ограничения продиктованы хранением в
|
||
# awgsetup_cfg.init (обёртка '...') и вложением в JSON: без кавычек и бэкслеша,
|
||
# без управляющих символов ([[:cntrl:]] целиком: ESC от стрелок в интерактивном
|
||
# вводе ломал бы JSON - je() контролы не экранирует), без пробелов по краям
|
||
# (клиент показал бы визуально пустое имя). Длина - до 128 БАЙТ в C-локали
|
||
# (LC_ALL=C даёт предсказуемый счёт на любой системной локали; 128 байт
|
||
# вмещают 64 кириллических символа UTF-8).
|
||
validate_server_name() {
|
||
local n="$1"
|
||
local LC_ALL=C
|
||
[[ -n "$n" ]] || return 1
|
||
(( ${#n} <= 128 )) || return 1
|
||
[[ "$n" == *"'"* || "$n" == *'"'* || "$n" == *'\'* ]] && return 1
|
||
[[ "$n" == *[[:cntrl:]]* ]] && return 1
|
||
[[ "$n" == " "* || "$n" == *" " ]] && return 1
|
||
return 0
|
||
}
|
||
|
||
# Срез пробелов по краям (для дружелюбия: случайный хвостовой пробел в
|
||
# --server-name или интерактивном вводе не должен валить установку).
|
||
_trim_ws() {
|
||
local s="$1"
|
||
s="${s#"${s%%[![:space:]]*}"}"
|
||
s="${s%"${s##*[![:space:]]}"}"
|
||
printf '%s' "$s"
|
||
}
|
||
|
||
# --mobile (D#38, полевой тест 26 jun): shorthand мобильного сетапа =
|
||
# --preset=mobile + порт 443/udp. Главная мобильная проблема - порт: на МТС
|
||
# дефолтный 39743/udp глушится, 443/udp (похож на QUIC/HTTP3) работает.
|
||
# Разворачивается в CLI_PRESET/CLI_PORT ДО их потребителей: явный --port
|
||
# пользователя выигрывает, противоречащий --preset - ошибка.
|
||
resolve_mobile_flag() {
|
||
[[ "${CLI_MOBILE:-0}" -eq 1 ]] || return 0
|
||
if [[ -n "${CLI_PRESET:-}" && "$CLI_PRESET" != "mobile" ]]; then
|
||
die "--mobile несовместим с --preset=${CLI_PRESET}: --mobile уже включает preset mobile."
|
||
fi
|
||
CLI_PRESET="mobile"
|
||
if [[ -z "$CLI_PORT" ]]; then
|
||
CLI_PORT=443
|
||
log "--mobile: порт 443/udp (мобильные операторы часто глушат нестандартные UDP-порты)."
|
||
fi
|
||
}
|
||
|
||
# Имя сервера в приложении Amnezia (D#180). Приоритет: CLI-флаг > сохранённый
|
||
# конфиг > интерактивный вопрос (первый запуск без --yes) > 'AWG Server'.
|
||
# Значение из конфига перепроверяется: файл могли править руками, а имя
|
||
# уходит в JSON vpn:// URI.
|
||
configure_server_name() {
|
||
local _name
|
||
if [[ -n "$CLI_SERVER_NAME" ]]; then
|
||
_name=$(_trim_ws "$CLI_SERVER_NAME")
|
||
validate_server_name "$_name" \
|
||
|| die "Некорректное --server-name: без кавычек, бэкслеша и управляющих символов, не длиннее 128 байт."
|
||
AWG_SERVER_NAME="$_name"
|
||
log "Имя сервера из CLI: ${AWG_SERVER_NAME}"
|
||
elif [[ -n "${AWG_SERVER_NAME:-}" ]]; then
|
||
_name=$(_trim_ws "$AWG_SERVER_NAME")
|
||
if validate_server_name "$_name"; then
|
||
AWG_SERVER_NAME="$_name"
|
||
else
|
||
log_warn "AWG_SERVER_NAME из $CONFIG_FILE не валидно, использую 'AWG Server'."
|
||
AWG_SERVER_NAME="AWG Server"
|
||
fi
|
||
elif [[ "${config_exists:-0}" -eq 1 || "$AUTO_YES" -eq 1 ]]; then
|
||
AWG_SERVER_NAME="AWG Server"
|
||
else
|
||
local input_name
|
||
while true; do
|
||
read -rp "Имя сервера в приложении Amnezia [AWG Server]: " input_name < /dev/tty
|
||
input_name=$(_trim_ws "$input_name")
|
||
if [[ -z "$input_name" ]]; then AWG_SERVER_NAME="AWG Server"; break; fi
|
||
if validate_server_name "$input_name"; then AWG_SERVER_NAME="$input_name"; break; fi
|
||
log_warn "Без кавычек, бэкслеша и управляющих символов, не длиннее 128 байт. Повторите ввод."
|
||
done
|
||
fi
|
||
export AWG_SERVER_NAME
|
||
}
|
||
|
||
# Guard смены подсети: [Peer]-блоки переносятся при переустановке как есть
|
||
# (render_server_config), их адреса выданы в СТАРОЙ подсети. Смена подсети
|
||
# под живыми клиентами ломает их: старые IPv4 могут выпасть из нового
|
||
# диапазона, а IPv6-суффиксы - столкнуться (decimal-кодировка /24 против
|
||
# hex у не-/24 масок даёт два пира с одним ::x). Поэтому при наличии пиров
|
||
# установка с другой подсетью прерывается (ревью PR #167). Самодостаточно
|
||
# (шаг 0, ДО загрузки awg_common.sh). Старая подсеть - первое значение
|
||
# Address в [Interface] awg0.conf: это нормализованный <network+1>/<prefix>,
|
||
# а новый AWG_TUNNEL_SUBNET к моменту вызова нормализован validate_subnet -
|
||
# строкового сравнения достаточно.
|
||
guard_subnet_change_with_peers() {
|
||
[[ -f "$SERVER_CONF_FILE" ]] || return 0
|
||
grep -q '^\[Peer\]' "$SERVER_CONF_FILE" 2>/dev/null || return 0
|
||
local old_subnet
|
||
# Address может быть dual-stack ("IPv4/n, IPv6/n") в любом порядке - берём
|
||
# именно IPv4-элемент, а не просто первый через запятую (иначе IPv6-первый
|
||
# Address дал бы ложную смену подсети). Нет IPv4 -> пусто -> fail-closed ниже.
|
||
old_subnet=$(sed -n 's/^[[:space:]]*Address[[:space:]]*=[[:space:]]*//p' "$SERVER_CONF_FILE" 2>/dev/null \
|
||
| tr ',' '\n' | sed 's/[[:space:]]//g' \
|
||
| grep -m1 -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$')
|
||
if [[ -z "$old_subnet" ]]; then
|
||
# Пиры есть, а старую подсеть определить нельзя - fail-closed: молчаливое
|
||
# продолжение перерендерило бы конфиг в новой подсети и сломало клиентов.
|
||
die "В ${SERVER_CONF_FILE} уже есть пиры, но строка Address в [Interface] не читается - проверить смену подсети невозможно. Восстановите Address, либо удалите клиентов (sudo bash $MANAGE_SCRIPT_PATH remove <имя>), либо --uninstall и чистая установка."
|
||
fi
|
||
if [[ "$old_subnet" != "$AWG_TUNNEL_SUBNET" ]]; then
|
||
die "Подсеть туннеля изменена (${old_subnet} -> ${AWG_TUNNEL_SUBNET}), но в ${SERVER_CONF_FILE} уже есть пиры: их адреса выданы в старой подсети, смена сломает клиентов. Варианты: оставьте прежнюю подсеть; удалите всех клиентов (sudo bash $MANAGE_SCRIPT_PATH remove <имя>); либо --uninstall и чистая установка."
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Валидация endpoint (FQDN / IPv4 / [IPv6]).
|
||
# Возвращает 0 если endpoint безопасен и попадает под один из форматов,
|
||
# иначе 1 (caller сам решает die или log_warn + unset).
|
||
# Запрещает newline/CR/quotes/backslash чтобы предотвратить injection в
|
||
# awgsetup_cfg.init и client.conf через --endpoint флаг (audit).
|
||
validate_endpoint() {
|
||
local ep="$1"
|
||
[[ -n "$ep" ]] || return 1
|
||
# Запрещаем символы которые могут сломать конфиг или внести injection
|
||
[[ "$ep" != *$'\n'* && "$ep" != *$'\r'* && \
|
||
"$ep" != *"'"* && "$ep" != *'"'* && "$ep" != *'\\'* && \
|
||
"$ep" != *' '* && "$ep" != *$'\t'* ]] || return 1
|
||
# Форма [IPv6]: структурная проверка содержимого скобок. Прежний charset-only
|
||
# пропускал мусор вроде [:::] / [1:2:3]. Зеркало _valid_ipv6 из awg_common.sh.
|
||
if [[ "$ep" == \[*\] ]]; then
|
||
local inner="${ep#\[}"; inner="${inner%\]}"
|
||
[[ "$inner" =~ ^[0-9A-Fa-f:]+$ ]] || return 1
|
||
case "$inner" in
|
||
*:::*|*::*::*) return 1 ;;
|
||
esac
|
||
[[ "$inner" == :* && "$inner" != ::* ]] && return 1
|
||
[[ "$inner" == *: && "$inner" != *:: ]] && return 1
|
||
local has_dcolon=0; [[ "$inner" == *::* ]] && has_dcolon=1
|
||
local IFS=':' parts=() p ngroups=0
|
||
read -ra parts <<< "$inner"
|
||
for p in "${parts[@]}"; do
|
||
[[ -z "$p" ]] && continue
|
||
[[ "$p" =~ ^[0-9A-Fa-f]{1,4}$ ]] || return 1
|
||
ngroups=$((ngroups + 1))
|
||
done
|
||
if [[ $has_dcolon -eq 1 ]]; then
|
||
(( ngroups <= 7 )) || return 1
|
||
else
|
||
(( ngroups == 8 )) || return 1
|
||
fi
|
||
return 0
|
||
fi
|
||
# Иначе FQDN или IPv4
|
||
[[ "$ep" =~ ^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*|[0-9]{1,3}(\.[0-9]{1,3}){3})$ ]] || return 1
|
||
# Если IPv4 формат - дополнительно проверяем диапазон октетов 0-255
|
||
if [[ "$ep" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
|
||
[[ "${BASH_REMATCH[1]}" -le 255 && "${BASH_REMATCH[2]}" -le 255 && \
|
||
"${BASH_REMATCH[3]}" -le 255 && "${BASH_REMATCH[4]}" -le 255 ]] || return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
validate_cidr_list() {
|
||
local input="$1" cidr o nospace
|
||
input="${input//$'\r'/}"
|
||
input="${input//$'\t'/ }"
|
||
# Перевод строки = инъекция в awgsetup_cfg.init (read <<< видит только первую
|
||
# строку, остальное прошло бы без проверки). Аналогично validate_endpoint.
|
||
[[ "$input" != *$'\n'* ]] || return 1
|
||
# Структурная проверка запятых до split: bash IFS отбрасывает хвостовой пустой
|
||
# элемент, поэтому '10.0.0.0/24,' раньше проходил. Отвергаем ведущую/хвостовую/
|
||
# двойную запятую и пустой ввод (пробелы игнорируем при этой проверке).
|
||
nospace="${input// /}"
|
||
case "$nospace" in
|
||
""|,*|*,|*,,*) return 1 ;;
|
||
esac
|
||
IFS=',' read -ra cidrs <<< "$input"
|
||
for cidr in "${cidrs[@]}"; do
|
||
cidr="${cidr// /}"
|
||
# Октеты без ведущих нулей; префикс 0-32 прямо в regex (без octal-арифметики).
|
||
if ! [[ "$cidr" =~ ^(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})\.(0|[1-9][0-9]{0,2})/([0-9]|[12][0-9]|3[0-2])$ ]]; then
|
||
return 1
|
||
fi
|
||
for o in "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}"; do
|
||
(( o <= 255 )) || return 1
|
||
done
|
||
done
|
||
}
|
||
|
||
configure_routing_mode() {
|
||
if [[ "$CLI_ROUTING_MODE" != "default" ]]; then
|
||
ALLOWED_IPS_MODE=$CLI_ROUTING_MODE
|
||
if [[ "$CLI_ROUTING_MODE" -eq 3 ]]; then
|
||
ALLOWED_IPS=$CLI_CUSTOM_ROUTES
|
||
if [ -z "$ALLOWED_IPS" ]; then die "Не указаны сети для --route-custom."; fi
|
||
fi
|
||
log "Режим маршрутизации из CLI: $ALLOWED_IPS_MODE"
|
||
elif [[ "$AUTO_YES" -eq 1 ]]; then
|
||
ALLOWED_IPS_MODE=2
|
||
log "Режим маршрутизации: Amnezia+DNS (--yes, по умолчанию)."
|
||
else
|
||
echo ""
|
||
log "Выберите режим маршрутизации (AllowedIPs клиента):"
|
||
echo " 1) Весь трафик (0.0.0.0/0) - Макс. приватность, может блокировать LAN"
|
||
echo " 2) Список Amnezia+DNS (умолч.) - Рекомендуется для обхода блокировок"
|
||
echo " 3) Только указанные сети (Split Tunneling)"
|
||
read -rp "Ваш выбор [2]: " r_mode < /dev/tty
|
||
ALLOWED_IPS_MODE=${r_mode:-2}
|
||
fi
|
||
case "$ALLOWED_IPS_MODE" in
|
||
1) ALLOWED_IPS="0.0.0.0/0"
|
||
log "Выбран режим: Весь трафик." ;;
|
||
3) if [[ -z "$CLI_CUSTOM_ROUTES" ]]; then
|
||
read -rp "Введите сети (a.b.c.d/xx,...): " ALLOWED_IPS < /dev/tty
|
||
while ! validate_cidr_list "$ALLOWED_IPS"; do
|
||
log_warn "Некорректный формат CIDR: '$ALLOWED_IPS'. Ожидается: x.x.x.x/y[,x.x.x.x/y]"
|
||
read -rp "Повторите ввод: " ALLOWED_IPS < /dev/tty
|
||
done
|
||
else
|
||
ALLOWED_IPS=$CLI_CUSTOM_ROUTES
|
||
if ! validate_cidr_list "$ALLOWED_IPS"; then
|
||
die "Некорректный формат CIDR: '$ALLOWED_IPS'. Ожидается: x.x.x.x/y[,x.x.x.x/y]"
|
||
fi
|
||
fi
|
||
log "Выбран режим: Пользовательский ($ALLOWED_IPS)" ;;
|
||
*) ALLOWED_IPS_MODE=2
|
||
# iOS рвёт туннель, если список начинается с 0.0.0.0/5: этот блок включает
|
||
# служебный 0.0.0.0/8, на котором ядро iOS спотыкается и не доходит до остальных
|
||
# маршрутов. 1.0.0.0/8 + 2.0.0.0/7 + 4.0.0.0/6 = тот же охват без нулевого блока
|
||
# (0.0.0.0/8 всё равно не маршрутизируется). Не возвращать к 0.0.0.0/5 (Issue #42).
|
||
ALLOWED_IPS="1.0.0.0/8, 2.0.0.0/7, 4.0.0.0/6, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/3, 160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, 172.64.0.0/10, 172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, 176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, 192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, 192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/4, 8.8.8.8/32, 1.1.1.1/32"
|
||
log "Выбран режим: Список Amnezia+DNS." ;;
|
||
esac
|
||
if [ -z "$ALLOWED_IPS" ]; then die "Не удалось определить AllowedIPs."; fi
|
||
export ALLOWED_IPS_MODE ALLOWED_IPS
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Генерация AWG 2.0 параметров (inline — нужны в шаге 0, до скачивания awg_common.sh)
|
||
# ==============================================================================
|
||
|
||
# Случайное число [min, max] через /dev/urandom (поддержка uint32)
|
||
rand_range() {
|
||
local min=$1 max=$2
|
||
local range=$((max - min + 1))
|
||
local random_val
|
||
random_val=$(od -An -tu4 -N4 /dev/urandom 2>/dev/null | tr -d ' ')
|
||
if [[ -z "$random_val" || ! "$random_val" =~ ^[0-9]+$ ]]; then
|
||
# Fallback: три $RANDOM (15 бит каждый) с XOR-перекрытием покрывают
|
||
# биты 0-30, т.е. весь [0, 2^31-1]. Прежний вариант (RANDOM<<15|RANDOM)
|
||
# давал только 30 бит - верхняя половина диапазона H никогда не выпадала.
|
||
random_val=$(( (RANDOM << 16) ^ (RANDOM << 8) ^ RANDOM ))
|
||
fi
|
||
echo $(( (random_val % range) + min ))
|
||
}
|
||
|
||
# Генерация 4 непересекающихся диапазонов для AWG H1-H4.
|
||
# Алгоритм: 8 случайных значений → sort → 4 пары (low, high).
|
||
# Сортировка даёт low <= high; строгие проверки ниже гарантируют зазор между
|
||
# парами (касание границ = пересечение в одной точке) и нижнюю границу >= 5
|
||
# (значения 1-4 зарезервированы под типы сообщений vanilla WireGuard).
|
||
# Минимальная ширина каждого диапазона = 1000 (для нормальной обфускации).
|
||
# Печатает 4 строки формата "low-high" в stdout.
|
||
# Возвращает 1 если за 20 попыток не удалось получить корректные диапазоны.
|
||
#
|
||
# Диапазон: [0, 2^31-1] = [0, 2147483647]. Спецификация AmneziaWG допускает
|
||
# полный uint32 (0-4294967295), но standalone Windows-клиент
|
||
# `amneziawg-windows-client` имеет UI-валидатор ограниченный 2^31-1 в
|
||
# `ui/syntax/highlighter.go:isValidHField()` (upstream bug
|
||
# amnezia-vpn/amneziawg-windows-client#85, не исправлен). Значения выше
|
||
# 2^31-1 на сервере работают, но клиентский редактор подчёркивает их
|
||
# красным и не даёт сохранять правки. Для совместимости генерируем в
|
||
# безопасной половине диапазона (#40).
|
||
#
|
||
# Оптимизация: один вызов `od -N32 -tu4` читает 32 байта = 8 uint32 значений
|
||
# одной операцией, вместо 8 отдельных subprocess через rand_range.
|
||
# Fallback на rand_range если /dev/urandom недоступен.
|
||
generate_awg_h_ranges() {
|
||
local attempt=0 max_attempts=20
|
||
while (( attempt < max_attempts )); do
|
||
local raw arr=() _v
|
||
raw=$(od -An -N32 -tu4 /dev/urandom 2>/dev/null | tr -s ' \n' '\n' | sed '/^$/d')
|
||
if [[ -n "$raw" ]]; then
|
||
local count=0
|
||
while IFS= read -r _v; do
|
||
[[ "$_v" =~ ^[0-9]+$ ]] || continue
|
||
# Маска 0x7FFFFFFF: очищает старший бит, значение в [0, 2^31-1]
|
||
# без bias (каждый младший бит независим).
|
||
arr+=("$(( _v & 2147483647 ))")
|
||
count=$((count + 1))
|
||
(( count == 8 )) && break
|
||
done <<< "$raw"
|
||
fi
|
||
if (( ${#arr[@]} != 8 )); then
|
||
arr=()
|
||
local _i
|
||
for _i in 1 2 3 4 5 6 7 8; do
|
||
arr+=("$(rand_range 0 2147483647)")
|
||
done
|
||
fi
|
||
local sorted
|
||
sorted=$(printf '%s\n' "${arr[@]}" | sort -n)
|
||
arr=()
|
||
while IFS= read -r _v; do arr+=("$_v"); done <<< "$sorted"
|
||
if (( ${arr[0]} >= 5 )) && \
|
||
(( ${arr[1]} - ${arr[0]} >= 1000 )) && \
|
||
(( ${arr[3]} - ${arr[2]} >= 1000 )) && \
|
||
(( ${arr[5]} - ${arr[4]} >= 1000 )) && \
|
||
(( ${arr[7]} - ${arr[6]} >= 1000 )) && \
|
||
(( ${arr[2]} > ${arr[1]} )) && \
|
||
(( ${arr[4]} > ${arr[3]} )) && \
|
||
(( ${arr[6]} > ${arr[5]} )); then
|
||
printf '%s-%s\n' "${arr[0]}" "${arr[1]}"
|
||
printf '%s-%s\n' "${arr[2]}" "${arr[3]}"
|
||
printf '%s-%s\n' "${arr[4]}" "${arr[5]}"
|
||
printf '%s-%s\n' "${arr[6]}" "${arr[7]}"
|
||
return 0
|
||
fi
|
||
attempt=$((attempt + 1))
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# Генерация CPS строки для I1
|
||
# Формат: "<r N>" где N — количество случайных байт (32-256)
|
||
generate_cps_i1() {
|
||
local n
|
||
n=$(rand_range 32 256)
|
||
echo "<r ${n}>"
|
||
}
|
||
|
||
# Генерация всех AWG 2.0 параметров
|
||
# Поддерживает --preset=default|mobile и точечные --jc/--jmin/--jmax overrides
|
||
generate_awg_params() {
|
||
local preset="${CLI_PRESET:-default}"
|
||
log "Генерация параметров AWG 2.0 (preset: $preset)..."
|
||
|
||
case "$preset" in
|
||
default)
|
||
# Jc 3-6: компромисс между обфускацией и совместимостью с мобильными (Discussion #38)
|
||
AWG_Jc=$(rand_range 3 6)
|
||
AWG_Jmin=$(rand_range 40 89)
|
||
# Jmax = Jmin + 50..250 (~90-339 байт, Issue #42)
|
||
AWG_Jmax=$(( AWG_Jmin + $(rand_range 50 250) ))
|
||
;;
|
||
mobile)
|
||
# Jc=3 фиксированный: alkorrnd (Tele2) — Jc=3 >95%, Jc=4 ~30%, Jc=5 <5%
|
||
# Узкий Jmax: markmokrenko (Yota) — Jmax=70 работает, Jmax>300 блокируется
|
||
AWG_Jc=3
|
||
AWG_Jmin=$(rand_range 30 50)
|
||
AWG_Jmax=$(( AWG_Jmin + $(rand_range 20 80) ))
|
||
log " Preset 'mobile': Jc=3, узкий Jmax для мобильных сетей"
|
||
;;
|
||
*)
|
||
die "Неизвестный preset: '$preset'. Допустимые: default, mobile"
|
||
;;
|
||
esac
|
||
|
||
# Точечные CLI overrides (поверх preset)
|
||
if [[ -n "${CLI_JC:-}" ]]; then
|
||
validate_jc_value "$CLI_JC" || die "Невалидный --jc=$CLI_JC (допустимо: 1-128)"
|
||
AWG_Jc="$CLI_JC"
|
||
fi
|
||
if [[ -n "${CLI_JMIN:-}" ]]; then
|
||
validate_junk_size "$CLI_JMIN" || die "Невалидный --jmin=$CLI_JMIN (допустимо: 0-1280)"
|
||
AWG_Jmin="$CLI_JMIN"
|
||
fi
|
||
if [[ -n "${CLI_JMAX:-}" ]]; then
|
||
validate_junk_size "$CLI_JMAX" || die "Невалидный --jmax=$CLI_JMAX (допустимо: 0-1280)"
|
||
AWG_Jmax="$CLI_JMAX"
|
||
fi
|
||
|
||
# Sanity: Jmax >= Jmin
|
||
if [[ "$AWG_Jmax" -lt "$AWG_Jmin" ]]; then
|
||
die "Jmax ($AWG_Jmax) не может быть меньше Jmin ($AWG_Jmin)"
|
||
fi
|
||
|
||
AWG_PRESET="$preset"
|
||
AWG_S1=$(rand_range 15 150)
|
||
AWG_S2=$(rand_range 15 150)
|
||
|
||
# Критическое ограничение из kernel: S1+56 != S2
|
||
# Предотвращает одинаковый размер init и response сообщений
|
||
while [[ $((AWG_S1 + 56)) -eq $AWG_S2 ]]; do
|
||
AWG_S2=$(rand_range 15 150)
|
||
done
|
||
|
||
# ⚠️ Нижние границы S3/S4 несовместимы с header protection из AmneziaWG 3.0.
|
||
# Там nonce для ChaCha20 нигде не передаётся, а берётся из первых 12 байт
|
||
# S-паддинга соответствующего сообщения (HEADER_PROTECTION_NONCE_SIZE = 12),
|
||
# поэтому обе реализации ОТКАЗЫВАЮТ в конфиге, если при заданном ключе
|
||
# защиты заголовков любой из S1-S4 меньше 12: модуль ядра возвращает -EINVAL
|
||
# (src/netlink.c, проверка has_protection && val16 < HEADER_PROTECTION_NONCE_SIZE),
|
||
# amneziawg-go отдаёт ошибку из device/uapi.go (есть с v3.0.0). То есть отказ
|
||
# ГРОМКИЙ, тихого ослабления шифра нет - проверено по исходникам 2 aug 2026.
|
||
# Пока мы на 2.0 и ключа защиты заголовков не ставим, эти диапазоны безопасны.
|
||
# ПРИ ВКЛЮЧЕНИИ header protection поднять обе нижние границы до 12, иначе
|
||
# часть установок просто не поднимет интерфейс. Держать в паре с гейтом
|
||
# _kernel_supports_awg3.
|
||
AWG_S3=$(rand_range 8 55)
|
||
|
||
# Вторая коллизия размеров: response+S2 != cookie+S3, то есть S3 != S2+28.
|
||
# Размеры сообщений (src/messages.h модуля ядра): init 148, response 92,
|
||
# cookie reply 64. Первые два измерены в проводе, cookie считается как
|
||
# 4 (header) + 4 (receiver_index) + 24 (nonce) + 32 (cookie 16 + authtag 16).
|
||
# Отсюда три условия совпадения итоговых размеров:
|
||
# init/response -> S2 = S1 + 56 (проверено циклом выше)
|
||
# response/cookie -> S3 = S2 + 28 (проверяем здесь)
|
||
# init/cookie -> S3 = S1 + 84 (недостижимо: минимум S1+84 = 99 при
|
||
# максимуме S3 = 55, цикл не нужен)
|
||
# Перегенерируем именно S3, а не S2: S2 уже прошёл проверку на S1+56.
|
||
while [[ $((AWG_S2 + 28)) -eq $AWG_S3 ]]; do
|
||
AWG_S3=$(rand_range 8 55)
|
||
done
|
||
|
||
AWG_S4=$(rand_range 4 27)
|
||
|
||
# H1-H4: 4 случайных непересекающихся uint32 диапазона.
|
||
# Рандомизация на каждую установку защищает от ТСПУ-фингерпринта
|
||
# по статическим H-значениям (Discussion #38, elvaleto/Klavishnik).
|
||
# Алгоритм: 8 случайных uint32 → sort → 4 непересекающиеся пары.
|
||
local _h_lines
|
||
mapfile -t _h_lines < <(generate_awg_h_ranges) || true
|
||
if [[ ${#_h_lines[@]} -ne 4 ]]; then
|
||
die "Не удалось сгенерировать H1-H4 диапазоны."
|
||
fi
|
||
AWG_H1="${_h_lines[0]}"
|
||
AWG_H2="${_h_lines[1]}"
|
||
AWG_H3="${_h_lines[2]}"
|
||
AWG_H4="${_h_lines[3]}"
|
||
|
||
# I1: CPS concealment
|
||
AWG_I1=$(generate_cps_i1)
|
||
|
||
# I2-I5 здесь НЕ генерируются (admin задаёт их вручную в awg0.conf, issue #71).
|
||
# Свежая генерация набора (первая установка или --preset/--jc/--jmin/--jmax)
|
||
# сбрасывает возможные stale I2-I5, загруженные из awgsetup_cfg.init, чтобы новый
|
||
# набор обфускации не тащил старые значения (--preset перегенерирует весь набор).
|
||
unset AWG_I2 AWG_I3 AWG_I4 AWG_I5
|
||
|
||
export AWG_Jc AWG_Jmin AWG_Jmax AWG_S1 AWG_S2 AWG_S3 AWG_S4 AWG_PRESET
|
||
export AWG_H1 AWG_H2 AWG_H3 AWG_H4 AWG_I1
|
||
|
||
log " Jc=$AWG_Jc, Jmin=$AWG_Jmin, Jmax=$AWG_Jmax"
|
||
log " S1=$AWG_S1, S2=$AWG_S2, S3=$AWG_S3, S4=$AWG_S4"
|
||
log " H1=$AWG_H1"
|
||
log " H2=$AWG_H2"
|
||
log " H3=$AWG_H3"
|
||
log " H4=$AWG_H4"
|
||
log " I1=$AWG_I1"
|
||
log "Параметры AWG 2.0 сгенерированы."
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Системная оптимизация (новое в v5.0)
|
||
# ==============================================================================
|
||
|
||
# Определение характеристик железа
|
||
detect_hardware() {
|
||
TOTAL_RAM_MB=$(awk '/MemTotal/ {printf "%.0f", $2/1024}' /proc/meminfo)
|
||
CPU_CORES=$(nproc)
|
||
MAIN_NIC=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1); exit}')
|
||
log "Железо: RAM=${TOTAL_RAM_MB}MB, CPU=${CPU_CORES} ядер, NIC=${MAIN_NIC}"
|
||
}
|
||
|
||
# _cleanup_package_list : имена пакетов, которые cleanup_system удалит на этой ОС.
|
||
# Один источник для очистки и для вопроса на шаге 0 - иначе они разъедутся.
|
||
# ⚠️ У OS_ID НЕТ дефолта намеренно: неизвестная ОС не должна получать разрушительный
|
||
# суперсет (snapd, lxd-agent-loader и снос каталогов снапов). Пусто = считаем не Ubuntu.
|
||
_cleanup_package_list() {
|
||
local list="modemmanager networkd-dispatcher unattended-upgrades packagekit udisks2"
|
||
[[ "${OS_ID:-}" == "ubuntu" ]] && list="snapd $list lxd-agent-loader"
|
||
printf '%s' "$list"
|
||
}
|
||
|
||
# _dpkg_usable : 0, если ответам dpkg можно верить.
|
||
# Отличить "пакет не установлен" от "база dpkg сломана" по коду возврата НЕЛЬЗЯ:
|
||
# замер на Ubuntu 24.04 дал rc=1 в обоих случаях. Поэтому спрашиваем про заведомо
|
||
# установленный пакет: не нашёлся и он - значит сломан сам механизм, а не пакеты.
|
||
# Без этой проверки пустой список молча пропустил бы вопрос, а шаг 1 позже отработал
|
||
# бы уже с исправным dpkg и снёс то, о чём не спрашивали.
|
||
_dpkg_usable() {
|
||
command -v dpkg-query >/dev/null 2>&1 || return 1
|
||
dpkg-query -W -f='${Status}' dpkg 2>/dev/null | grep -q "ok installed"
|
||
}
|
||
|
||
# _cloud_init_removable : 0, если установленный cloud-init действительно будет удалён.
|
||
# cloud-init стоит отдельно от списка выше: его удаляют только когда он НЕ управляет
|
||
# сетью. Ответ нужен в двух местах - в самой очистке и в вопросе на шаге 0, поэтому он
|
||
# живёт здесь. Иначе согласие спрашивалось бы про один набор, а удалялся другой, то есть
|
||
# ровно та претензия, из-за которой заведён issue #213.
|
||
# 🔴 Любая НЕудача проверки означает "управляет сетью, не трогаем". Цена ошибок
|
||
# несимметрична: лишний оставленный cloud-init стоит десятков мегабайт, а удалённый по
|
||
# ошибке - потери сети после перезагрузки на удалённом сервере. Поэтому от ls по маске
|
||
# здесь отказались: он отдаёт rc=2 и когда каталога нет, и когда маска не совпала.
|
||
_cloud_init_removable() {
|
||
dpkg-query -W -f='${Status}' cloud-init 2>/dev/null | grep -q "ok installed" || return 1
|
||
local f
|
||
if [ -d /etc/netplan ]; then
|
||
[ -r /etc/netplan ] || return 1
|
||
for f in /etc/netplan/*cloud-init*; do
|
||
[ -e "$f" ] && return 1
|
||
done
|
||
grep -rq "cloud-init" /etc/netplan/ 2>/dev/null
|
||
case $? in
|
||
0) return 1 ;;
|
||
1) : ;;
|
||
*) return 1 ;;
|
||
esac
|
||
fi
|
||
if [ -f /etc/network/interfaces ] && grep -q "cloud-init" /etc/network/interfaces 2>/dev/null; then
|
||
return 1
|
||
fi
|
||
# На Debian cloud-init пишет именно сюда, а не в основной файл.
|
||
if [ -d /etc/network/interfaces.d ] \
|
||
&& grep -rq "cloud-init" /etc/network/interfaces.d/ 2>/dev/null; then
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# _snaps_dir_readable : 0, если каталог снапов есть и читается.
|
||
# Пустой ответ _user_snaps при нечитаемом каталоге означает "не смог посмотреть", а не
|
||
# "снапов нет". Путать эти случаи нельзя: от них зависит дефолт разрушительного вопроса.
|
||
_snaps_dir_readable() {
|
||
[ -d /var/lib/snapd/snaps ] && [ -r /var/lib/snapd/snaps ]
|
||
}
|
||
|
||
# _user_snaps : имена снапов, поставленных ПОЛЬЗОВАТЕЛЕМ, по одному в строке, БЕЗ ПОВТОРОВ.
|
||
# В каталоге лежит по файлу на КАЖДУЮ удержанную ревизию, поэтому без dedup обновлённый
|
||
# однажды снап попал бы в предупреждение несколько раз подряд.
|
||
# Базовыми считаем только те, что не несут пользовательских данных: snapd, bare и core*.
|
||
# 🔴 lxd НЕ фильтруем: LXD из снапа держит контейнеры и их данные в /var/snap/lxd, и
|
||
# назвать такой хост "терять нечего" значит молча снести их.
|
||
# ⚠️ Читаем файлы, а не вывод 'snap list': бинарь snap к этому моменту мог быть уже снесён
|
||
# прошлым прогоном, и список молча оказался бы пуст.
|
||
_user_snaps() {
|
||
local f name
|
||
for f in /var/lib/snapd/snaps/*.snap; do
|
||
[ -e "$f" ] || continue
|
||
name="${f##*/}"; name="${name%_*.snap}"
|
||
case "$name" in
|
||
snapd|bare|core|core[0-9]*) continue ;;
|
||
esac
|
||
printf '%s\n' "$name"
|
||
done | sort -u
|
||
}
|
||
|
||
# Согласие на удаление системных пакетов (issue #213). Спрашиваем на ШАГЕ 0, где и так
|
||
# задаются остальные вопросы: дальше установка должна идти без участия человека.
|
||
# Ответ сохраняется в awgsetup_cfg.init, чтобы повторный или возобновлённый запуск не
|
||
# спрашивал заново и, главное, не считал молчание согласием.
|
||
configure_package_cleanup() {
|
||
[[ "$NO_TWEAKS" -eq 1 ]] && return 0
|
||
# Решение уже есть: флаг командной строки или запись из прошлого запуска.
|
||
[[ -n "$KEEP_PACKAGES" ]] && return 0
|
||
|
||
if ! _dpkg_usable; then
|
||
KEEP_PACKAGES=1
|
||
log_warn "Опросить dpkg не удалось, поэтому системные пакеты трогать не буду."
|
||
return 0
|
||
fi
|
||
|
||
local installed=() pkg
|
||
for pkg in $(_cleanup_package_list); do
|
||
if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then
|
||
installed+=("$pkg")
|
||
fi
|
||
done
|
||
# cloud-init удаляется отдельной веткой очистки и только когда не управляет сетью,
|
||
# поэтому в список берём его по тому же условию: спросить надо ровно про то, что
|
||
# реально удалят.
|
||
if _cloud_init_removable; then
|
||
installed+=("cloud-init")
|
||
fi
|
||
|
||
# Каталоги снапов сносит отдельный rm -rf, и он НЕ зависит от того, попал ли snapd
|
||
# в список выше: пакет мог остаться в состоянии 'deinstall ok config-files'.
|
||
local snap_dirs=0
|
||
if [[ "${OS_ID:-}" == "ubuntu" ]] && { [ -d /snap ] || [ -d /var/snap ]; }; then
|
||
snap_dirs=1
|
||
fi
|
||
|
||
if [ ${#installed[@]} -eq 0 ] && [ "$snap_dirs" -eq 0 ]; then
|
||
KEEP_PACKAGES=0
|
||
return 0
|
||
fi
|
||
|
||
# Свои снапы ищем только когда им что-то грозит: на Debian snapd в список не входит.
|
||
local snaps="" snaps_unknown=0
|
||
if [ "$snap_dirs" -eq 1 ] || [[ " ${installed[*]} " == *" snapd "* ]]; then
|
||
if _snaps_dir_readable; then
|
||
snaps="$(_user_snaps | tr '\n' ' ')"; snaps="${snaps% }"
|
||
else
|
||
snaps_unknown=1
|
||
fi
|
||
fi
|
||
|
||
log_warn "Сервер настраивается как однозадачный, поэтому будут удалены пакеты:"
|
||
[ ${#installed[@]} -gt 0 ] && log_warn " ${installed[*]}"
|
||
if [ "$snap_dirs" -eq 1 ]; then
|
||
log_warn " Плюс каталоги /snap, /var/snap и /var/lib/snapd со всеми снапами и их данными."
|
||
if [ "$snaps_unknown" -eq 1 ]; then
|
||
log_warn " Что именно у вас установлено, проверить не удалось: каталог снапов недоступен."
|
||
elif [[ -n "$snaps" ]]; then
|
||
log_warn " Ваши снапы, которые будут потеряны: $snaps"
|
||
fi
|
||
fi
|
||
if [[ " ${installed[*]} " == *" cloud-init "* ]]; then
|
||
log_warn " Вместе с cloud-init удаляются каталоги /etc/cloud и /var/lib/cloud."
|
||
fi
|
||
|
||
if [[ "$AUTO_YES" -eq 1 ]]; then
|
||
KEEP_PACKAGES=0
|
||
log "Удаление подтверждено автоматически (--yes). Сохранить пакеты: --keep-packages."
|
||
return 0
|
||
fi
|
||
|
||
# Есть что терять - удаляем ТОЛЬКО по явному "да" (allowlist, как у остальных
|
||
# разрушительных вопросов скрипта). Прежний вариант проверял ответ на букву n, и
|
||
# тогда "нет", "не", "ok" и любая случайная клавиша означали УДАЛИТЬ.
|
||
local risky=0
|
||
if [[ -n "$snaps" ]] || [ "$snaps_unknown" -eq 1 ]; then risky=1; fi
|
||
|
||
local answer="" hint="[Y/n]"
|
||
[ "$risky" -eq 1 ] && hint="[y/N]"
|
||
if ! read -rp "Удалить эти пакеты? $hint: " answer < /dev/tty; then
|
||
KEEP_PACKAGES=1
|
||
log_warn "Терминал недоступен, вопрос задать не смог - пакеты сохраняю."
|
||
return 0
|
||
fi
|
||
# Обрезаем пробелы и CR: ответ из putty приходит с \r.
|
||
answer="$(printf '%s' "$answer" | tr -d '[:space:]')"
|
||
|
||
if [ "$risky" -eq 1 ]; then
|
||
case "$answer" in
|
||
[Yy]|[Yy][Ee][Ss]|да|Да|ДА|д|Д) KEEP_PACKAGES=0 ;;
|
||
*) KEEP_PACKAGES=1 ;;
|
||
esac
|
||
else
|
||
case "$answer" in
|
||
[Nn]|[Nn][Oo]|нет|Нет|НЕТ|не|Не|н|Н) KEEP_PACKAGES=1 ;;
|
||
*) KEEP_PACKAGES=0 ;;
|
||
esac
|
||
fi
|
||
|
||
if [[ "$KEEP_PACKAGES" -eq 1 ]]; then
|
||
log "Пакеты сохраняются. Фаервол, Fail2Ban и оптимизация при этом остаются."
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Удаление ненужных пакетов и сервисов
|
||
cleanup_system() {
|
||
log "Очистка системы от ненужных компонентов..."
|
||
|
||
# Снимок default route ДО очистки - для проверки что мы не сломали сеть.
|
||
# Issue #84: на чистой Ubuntu 26.04 server (subiquity, без cloud-init
|
||
# netplan-маркеров) apt-get autoremove после purge cloud-init сносил
|
||
# netplan-generator как transitive dep, и сервер терял IP после reboot.
|
||
local pre_default_route
|
||
pre_default_route="$(ip -4 route show default 2>/dev/null | head -1 || true)"
|
||
log_debug "Pre-cleanup default route: ${pre_default_route:-<none>}"
|
||
|
||
# apt-mark hold на критичные пакеты сетевого стека: защита от случайного
|
||
# удаления через transitive deps. Покрываем оба варианта именования netplan
|
||
# (netplan.io на 24.04, netplan-generator на 25.10/26.04), а также
|
||
# systemd-resolved и netcfg/ifupdown legacy. Пакета systemd-networkd
|
||
# отдельно не существует - бинарь живёт внутри systemd, hold-ить нечего.
|
||
# Перед hold снимаем снимок текущих hold-ов пользователя, чтобы при unhold
|
||
# не затереть его pre-existing holds (например на linux-image-*).
|
||
local _hold_pkgs="netplan.io netplan-generator systemd-resolved netcfg ifupdown"
|
||
local _preexisting_holds=""
|
||
_preexisting_holds="$(apt-mark showhold 2>/dev/null || true)"
|
||
local _held_actual=()
|
||
local _hpkg
|
||
for _hpkg in $_hold_pkgs; do
|
||
if dpkg-query -W -f='${Status}' "$_hpkg" 2>/dev/null | grep -q "ok installed"; then
|
||
# Пропускаем уже залоченные пользователем - их hold не наш и снимать его нельзя.
|
||
if grep -qxF "$_hpkg" <<<"$_preexisting_holds"; then
|
||
continue
|
||
fi
|
||
apt-mark hold "$_hpkg" >/dev/null 2>&1 && _held_actual+=("$_hpkg")
|
||
fi
|
||
done
|
||
[ ${#_held_actual[@]} -gt 0 ] && log_debug "Apt-mark hold: ${_held_actual[*]}"
|
||
|
||
# Пакеты для удаления (безопасные для VPS)
|
||
# snapd и lxd-agent-loader — только на Ubuntu, на Debian их нет
|
||
local packages_to_remove=()
|
||
local pkg
|
||
local cleanup_list
|
||
cleanup_list="$(_cleanup_package_list)"
|
||
for pkg in $cleanup_list; do
|
||
if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then
|
||
packages_to_remove+=("$pkg")
|
||
fi
|
||
done
|
||
|
||
if [ ${#packages_to_remove[@]} -gt 0 ]; then
|
||
log "Удаление: ${packages_to_remove[*]}"
|
||
DEBIAN_FRONTEND=noninteractive apt-get purge -y "${packages_to_remove[@]}" || log_warn "Ошибка удаления некоторых пакетов"
|
||
fi
|
||
|
||
# Очистка snap артефактов (только Ubuntu)
|
||
if [[ "${OS_ID:-}" == "ubuntu" && -d /snap ]]; then
|
||
log "Очистка snap артефактов..."
|
||
rm -rf /snap /var/snap /var/lib/snapd 2>/dev/null || log_warn "Ошибка очистки snap"
|
||
fi
|
||
|
||
# cloud-init: удалять только если НЕ управляет сетью
|
||
# Консервативный подход: сначала проверяем маркеры cloud-init, затем renderer
|
||
if dpkg-query -W -f='${Status}' cloud-init 2>/dev/null | grep -q "ok installed"; then
|
||
if _cloud_init_removable; then
|
||
log "Удаление cloud-init (сеть не зависит от него)..."
|
||
DEBIAN_FRONTEND=noninteractive apt-get purge -y cloud-init 2>/dev/null || log_warn "Ошибка удаления cloud-init"
|
||
rm -rf /etc/cloud /var/lib/cloud 2>/dev/null
|
||
else
|
||
log_warn "cloud-init управляет сетью — пропускаем удаление."
|
||
fi
|
||
fi
|
||
|
||
# apt-get autoremove убран (был источник Issue #84 на Ubuntu 26.04 ISO):
|
||
# autoremove зачищал netplan-generator как transitive dep cloud-init.
|
||
# Орфанные пакеты после purge займут ~50-200 МБ - приемлемо ради стабильности.
|
||
# Пользователь может вручную: apt-get autoremove --no-install-recommends.
|
||
|
||
# Снимаем hold, чтобы не оставлять "застывшие" пакеты в состоянии hold.
|
||
local _upkg
|
||
for _upkg in "${_held_actual[@]}"; do
|
||
apt-mark unhold "$_upkg" >/dev/null 2>&1 || true
|
||
done
|
||
|
||
# Проверка что default route не пропал. Если пропал - пробуем восстановить.
|
||
# Восстанавливаем netplan.io безусловно (есть на всех supported distro),
|
||
# netplan-generator - только если он реально доступен в архивах данной
|
||
# системы (на Debian 12 этого пакета ещё нет, apt-get install <missing>
|
||
# абортит транзакцию даже с || true для всей строки).
|
||
local post_default_route
|
||
post_default_route="$(ip -4 route show default 2>/dev/null | head -1 || true)"
|
||
if [[ -n "$pre_default_route" && -z "$post_default_route" ]]; then
|
||
log_error "Маршрут по умолчанию потерян после очистки. Попытка восстановления..."
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||
netplan.io 2>/dev/null || true
|
||
if apt-cache show netplan-generator &>/dev/null; then
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||
netplan-generator 2>/dev/null || true
|
||
fi
|
||
systemctl restart systemd-networkd 2>/dev/null || true
|
||
netplan apply 2>/dev/null || true
|
||
# Цикл ожидания маршрута: до ~26 секунд, проверка раз в 1-5 секунд.
|
||
# Фиксированные sleep не годятся - время появления DHCP-маршрута
|
||
# на медленных VM непредсказуемо.
|
||
local _wait
|
||
for _wait in 1 2 3 5 5 5 5; do
|
||
post_default_route="$(ip -4 route show default 2>/dev/null | head -1 || true)"
|
||
[[ -n "$post_default_route" ]] && break
|
||
sleep "$_wait"
|
||
done
|
||
# Last-ditch: поднять интерфейс из pre_default_route. Сначала пробуем
|
||
# networkctl renew (для systemd-networkd-managed link), если маршрут не
|
||
# появился - переходим на dhclient (для ifupdown-managed link).
|
||
if [[ -z "$post_default_route" ]]; then
|
||
local _iface
|
||
_iface="$(awk '{for (i=1; i<=NF; i++) if ($i == "dev") { print $(i+1); exit } }' <<<"$pre_default_route")"
|
||
if [[ -n "$_iface" ]]; then
|
||
log_warn "Финальная попытка поднять интерфейс $_iface..."
|
||
ip link set "$_iface" up 2>/dev/null || true
|
||
if command -v networkctl &>/dev/null; then
|
||
networkctl renew "$_iface" 2>/dev/null || true
|
||
sleep 3
|
||
post_default_route="$(ip -4 route show default 2>/dev/null | head -1 || true)"
|
||
fi
|
||
# Если networkctl не привёл маршрут к жизни (или его нет) - dhclient.
|
||
if [[ -z "$post_default_route" ]] && command -v dhclient &>/dev/null; then
|
||
dhclient -4 "$_iface" 2>/dev/null || true
|
||
sleep 3
|
||
post_default_route="$(ip -4 route show default 2>/dev/null | head -1 || true)"
|
||
fi
|
||
fi
|
||
fi
|
||
if [[ -z "$post_default_route" ]]; then
|
||
die "Сеть не восстановилась после cleanup_system. Восстановите её с консоли (например: sudo dhclient -4 <интерфейс>) и перезапустите установщик с флагом --no-tweaks."
|
||
fi
|
||
log_warn "Сеть восстановлена: $post_default_route"
|
||
fi
|
||
|
||
log "Очистка системы завершена."
|
||
}
|
||
|
||
# Настройка swap
|
||
optimize_swap() {
|
||
log "Оптимизация swap..."
|
||
local target_swap_mb
|
||
|
||
if [[ $TOTAL_RAM_MB -le 2048 ]]; then
|
||
target_swap_mb=1024
|
||
else
|
||
target_swap_mb=512
|
||
fi
|
||
|
||
# Проверяем текущий swap
|
||
local current_swap_mb
|
||
current_swap_mb=$(free -m | awk '/Swap:/ {print $2}')
|
||
|
||
if [[ $current_swap_mb -ge $target_swap_mb ]]; then
|
||
log "Swap уже достаточен: ${current_swap_mb}MB (цель: ${target_swap_mb}MB)"
|
||
else
|
||
log "Создание swap файла: ${target_swap_mb}MB"
|
||
# Отключаем существующий swap файл если есть
|
||
if [[ -f /swapfile ]]; then
|
||
swapoff /swapfile 2>/dev/null
|
||
rm -f /swapfile
|
||
fi
|
||
dd if=/dev/zero of=/swapfile bs=1M count="$target_swap_mb" status=none 2>/dev/null || {
|
||
log_warn "Ошибка создания swap файла"
|
||
return 1
|
||
}
|
||
chmod 600 /swapfile
|
||
mkswap /swapfile >/dev/null 2>&1 || { log_warn "Ошибка mkswap"; return 1; }
|
||
swapon /swapfile || { log_warn "Ошибка swapon"; return 1; }
|
||
# Добавляем в fstab если отсутствует. Точная проверка по полям:
|
||
# игнорируем закомментированные строки и partial matches (например,
|
||
# `/swapfile.bak` или старая строка в комментарии).
|
||
if ! awk '!/^[[:space:]]*#/ && $1 == "/swapfile" && $3 == "swap" {found=1} END {exit !(found+0)}' \
|
||
/etc/fstab; then
|
||
# Гарантируем перевод строки в конце файла. Без него запись
|
||
# приклеится к последней строке fstab, и та станет некорректной:
|
||
# получится одна строка из 11 полей вместо шести. Подстановка
|
||
# команды срезает завершающие переводы строки, поэтому у
|
||
# нормально завершённого файла проверка даёт пустую строку и
|
||
# лишний перевод НЕ добавляется.
|
||
if [[ -s /etc/fstab && -n "$(tail -c1 /etc/fstab)" ]]; then
|
||
echo >> /etc/fstab
|
||
fi
|
||
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||
fi
|
||
log "Swap файл создан: ${target_swap_mb}MB"
|
||
fi
|
||
|
||
# Настройка swappiness
|
||
sysctl -w vm.swappiness=10 >/dev/null 2>&1
|
||
}
|
||
|
||
# Оптимизация сетевого интерфейса
|
||
optimize_nic() {
|
||
if [[ -z "$MAIN_NIC" ]]; then
|
||
log_warn "Основной NIC не определён, пропуск оптимизации."
|
||
return 1
|
||
fi
|
||
|
||
if ! command -v ethtool &>/dev/null; then
|
||
log_debug "ethtool не найден, пропуск NIC оптимизации."
|
||
return 0
|
||
fi
|
||
|
||
log "Оптимизация NIC: $MAIN_NIC"
|
||
# Отключение GRO/GSO/TSO — могут мешать VPN-трафику
|
||
ethtool -K "$MAIN_NIC" gro off 2>/dev/null || log_debug "GRO: не поддерживается/уже выкл."
|
||
ethtool -K "$MAIN_NIC" gso off 2>/dev/null || log_debug "GSO: не поддерживается/уже выкл."
|
||
ethtool -K "$MAIN_NIC" tso off 2>/dev/null || log_debug "TSO: не поддерживается/уже выкл."
|
||
log "NIC оптимизация завершена."
|
||
}
|
||
|
||
# Полная оптимизация системы
|
||
optimize_system() {
|
||
log "Оптимизация системы под VPN-сервер..."
|
||
detect_hardware
|
||
optimize_swap
|
||
optimize_nic
|
||
log "Оптимизация системы завершена."
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Настройка sysctl (минимальная, для --no-tweaks)
|
||
# ==============================================================================
|
||
|
||
setup_minimal_sysctl() {
|
||
log "Настройка минимального sysctl (--no-tweaks)..."
|
||
local f="/etc/sysctl.d/99-amneziawg-forwarding.conf"
|
||
cat > "$f" << SYSEOF
|
||
# AmneziaWG — минимальные настройки (--no-tweaks)
|
||
net.ipv4.ip_forward = 1
|
||
SYSEOF
|
||
if [[ "${DISABLE_IPV6:-1}" -eq 1 ]]; then
|
||
cat >> "$f" << SYSEOF
|
||
net.ipv6.conf.all.disable_ipv6 = 1
|
||
net.ipv6.conf.default.disable_ipv6 = 1
|
||
net.ipv6.conf.lo.disable_ipv6 = 1
|
||
SYSEOF
|
||
else
|
||
cat >> "$f" << SYSEOF
|
||
net.ipv6.conf.all.forwarding = 1
|
||
SYSEOF
|
||
fi
|
||
sysctl -p "$f" >/dev/null 2>&1 || log_warn "Ошибка sysctl -p"
|
||
log "Минимальный sysctl настроен."
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Настройка sysctl (расширенная)
|
||
# ==============================================================================
|
||
|
||
setup_advanced_sysctl() {
|
||
log "Настройка sysctl..."
|
||
local f="/etc/sysctl.d/99-amneziawg-security.conf"
|
||
|
||
# Адаптивные буферы по объёму RAM
|
||
local rmem_max wmem_max netdev_backlog
|
||
if [[ ${TOTAL_RAM_MB:-1024} -ge 2048 ]]; then
|
||
rmem_max=16777216 # 16MB
|
||
wmem_max=16777216
|
||
netdev_backlog=5000
|
||
else
|
||
rmem_max=4194304 # 4MB
|
||
wmem_max=4194304
|
||
netdev_backlog=2500
|
||
fi
|
||
|
||
cat > "$f" << EOF
|
||
# AmneziaWG 2.0 Security/Performance Settings - $(date)
|
||
# Автоматически сгенерировано install_amneziawg.sh v${SCRIPT_VERSION}
|
||
|
||
# --- IP Forwarding ---
|
||
net.ipv4.ip_forward = 1
|
||
$(if [[ "${DISABLE_IPV6:-1}" -eq 1 ]]; then
|
||
echo "net.ipv6.conf.all.disable_ipv6 = 1"
|
||
echo "net.ipv6.conf.default.disable_ipv6 = 1"
|
||
echo "net.ipv6.conf.lo.disable_ipv6 = 1"
|
||
else
|
||
echo "# IPv6 не отключен"
|
||
echo "net.ipv6.conf.all.forwarding = 1"
|
||
fi)
|
||
|
||
# --- TCP/IP Hardening ---
|
||
# rp_filter = 2 (loose mode): проверяет source IP по ANY маршруту в таблице,
|
||
# а не по обратному маршруту через тот же интерфейс. Strict mode (=1) ломает
|
||
# routing на облачных хостерах (Hetzner и подобных) где шлюз в другой подсети,
|
||
# чем IP самой VPS — ответные пакеты не проходят strict reverse path check.
|
||
# Loose mode безопасен: подделанные source IP всё равно отсеиваются если для
|
||
# них нет маршрута вообще. Discussion #41 (z036).
|
||
net.ipv4.conf.all.rp_filter = 2
|
||
net.ipv4.conf.default.rp_filter = 2
|
||
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||
net.ipv4.icmp_ignore_bogus_error_responses = 1
|
||
net.ipv4.tcp_syncookies = 1
|
||
net.ipv4.tcp_max_syn_backlog = 4096
|
||
net.ipv4.tcp_synack_retries = 2
|
||
net.ipv4.tcp_syn_retries = 5
|
||
net.ipv4.tcp_rfc1337 = 1
|
||
|
||
# --- Redirects ---
|
||
net.ipv4.conf.all.accept_redirects = 0
|
||
net.ipv4.conf.default.accept_redirects = 0
|
||
net.ipv4.conf.all.secure_redirects = 0
|
||
net.ipv4.conf.default.secure_redirects = 0
|
||
net.ipv4.conf.all.send_redirects = 0
|
||
net.ipv4.conf.default.send_redirects = 0
|
||
net.ipv4.conf.all.accept_source_route = 0
|
||
net.ipv4.conf.default.accept_source_route = 0
|
||
$(if [[ "${DISABLE_IPV6:-1}" -ne 1 ]]; then
|
||
echo "net.ipv6.conf.all.accept_redirects = 0"
|
||
echo "net.ipv6.conf.default.accept_redirects = 0"
|
||
fi)
|
||
|
||
# --- BBR Congestion Control ---
|
||
net.core.default_qdisc = fq
|
||
net.ipv4.tcp_congestion_control = bbr
|
||
|
||
# --- Network Buffers (adaptive) ---
|
||
net.core.rmem_max = ${rmem_max}
|
||
net.core.wmem_max = ${wmem_max}
|
||
net.core.netdev_max_backlog = ${netdev_backlog}
|
||
|
||
# --- Conntrack ---
|
||
net.netfilter.nf_conntrack_max = 65536
|
||
|
||
# --- Security ---
|
||
vm.swappiness = 10
|
||
kernel.sysrq = 0
|
||
|
||
# Подавление kernel warning/notice messages в VNC-консоли хостера.
|
||
# Без этого fail2ban UFW-блокировки спамят VNC окно строками типа
|
||
# "[UFW BLOCK]" и делают консоль непригодной для работы.
|
||
# Format: console_loglevel default_msg_loglevel min_console_loglevel default_console_loglevel
|
||
# Значение 3 = KERN_ERR — на консоль идут только ошибки и критические.
|
||
# Discussion #41 (z036).
|
||
kernel.printk = 3 4 1 3
|
||
EOF
|
||
|
||
log "Применение sysctl..."
|
||
if ! sysctl -p "$f" >/dev/null 2>&1; then
|
||
# nf_conntrack может быть недоступен до загрузки модуля
|
||
log_warn "Некоторые параметры sysctl не применились (nf_conntrack будет доступен позже)."
|
||
sysctl -p "$f" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Фаервол и безопасность
|
||
# ==============================================================================
|
||
|
||
# Определение реального SSH-порта(ов) для корректного правила UFW.
|
||
# Без этого ufw limit 22/tcp + default deny incoming отрезает доступ к серверу
|
||
# после ufw enable, если SSH поднят на нестандартном порту (Issue #91).
|
||
# Функция самодостаточна: вызывается на шаге 4, ДО подключения awg_common.sh.
|
||
# Источники:
|
||
# 1. CLI_SSH_PORT (--ssh-port=, ручной override, список через запятую) - авторитетно
|
||
# иначе ОБЪЕДИНЕНИЕ (union, не fallback - так не пропустим реальный порт):
|
||
# 2. sshd -T (эффективный конфиг: `Port` И `ListenAddress host:port`, учитывает drop-ins)
|
||
# 3. ss -tlnp (реальные listening-сокеты sshd: ground truth для ListenAddress)
|
||
# 4. /etc/ssh/sshd_config + sshd_config.d/*.conf (парсинг, только если 2-3 пусты)
|
||
# 5. 22 (дефолт, если ничего не найдено)
|
||
# Выводит уникальные валидные порты (1-65535) через пробел в stdout.
|
||
# ВАЖНО: внутри только log_warn/log_error (stderr); log() пишет в stdout и
|
||
# испортил бы перехват $(detect_ssh_ports).
|
||
detect_ssh_ports() {
|
||
local ports="" p pp valid=""
|
||
# awk: достаёт порт из строк `port N` и `listenaddress host:port`
|
||
# (IPv4 и [IPv6]); голый адрес без порта пропускается.
|
||
local awk_ports='tolower($1)=="port"&&$2~/^[0-9]+$/{print $2} tolower($1)=="listenaddress"{v=$2; if(v~/\]:[0-9]+$/){sub(/.*\]:/,"",v); print v} else if(v~/^[0-9.]+:[0-9]+$/){sub(/.*:/,"",v); print v}}'
|
||
|
||
if [[ -n "$CLI_SSH_PORT" ]]; then
|
||
# 1. Ручной override - авторитетный источник
|
||
ports="${CLI_SSH_PORT//,/ }"
|
||
else
|
||
# 2. sshd -T: эффективная конфигурация (Port + ListenAddress, drop-ins)
|
||
if command -v sshd &>/dev/null; then
|
||
ports+=" $(sshd -T 2>/dev/null | awk "$awk_ports" | tr '\n' ' ')"
|
||
fi
|
||
# 3. ss: реальные listening-сокеты sshd. Объединяем, не fallback -
|
||
# ловит ListenAddress-порт, даже если sshd -T печатает дефолтный port 22.
|
||
if command -v ss &>/dev/null; then
|
||
ports+=" $(ss -H -tlnp 2>/dev/null | awk '/"sshd"/{n=split($4,a,":"); print a[n]}' | tr '\n' ' ')"
|
||
fi
|
||
# 4. Парсинг конфигов - только если sshd -T и ss ничего не дали
|
||
if [[ -z "${ports// }" ]]; then
|
||
local cfgs=() d
|
||
[[ -f /etc/ssh/sshd_config ]] && cfgs+=(/etc/ssh/sshd_config)
|
||
for d in /etc/ssh/sshd_config.d/*.conf; do
|
||
[[ -f "$d" ]] && cfgs+=("$d")
|
||
done
|
||
if [[ "${#cfgs[@]}" -gt 0 ]]; then
|
||
ports+=" $(awk "$awk_ports" "${cfgs[@]}" 2>/dev/null | tr '\n' ' ')"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Валидация (десятичная 1-65535, 10# против octal) + дедуп с сохранением порядка
|
||
for p in $ports; do
|
||
if [[ "$p" =~ ^[0-9]+$ ]]; then
|
||
pp=$((10#$p))
|
||
if (( pp >= 1 && pp <= 65535 )); then
|
||
case " $valid " in
|
||
*" $pp "*) ;;
|
||
*) valid+="${valid:+ }$pp" ;;
|
||
esac
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# 5. Дефолт, если детект ничего валидного не дал
|
||
if [[ -z "$valid" ]]; then
|
||
[[ -n "$CLI_SSH_PORT" ]] && log_warn "--ssh-port не содержит валидных портов, использую 22."
|
||
valid="22"
|
||
fi
|
||
printf '%s' "$valid"
|
||
}
|
||
|
||
setup_improved_firewall() {
|
||
log "Настройка UFW..."
|
||
if ! command -v ufw &>/dev/null; then install_packages ufw; fi
|
||
|
||
# Определяем основной сетевой интерфейс для правила маршрутизации
|
||
local main_nic
|
||
main_nic=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1); exit}')
|
||
if [[ -z "$main_nic" ]]; then
|
||
log_warn "Не удалось определить сетевой интерфейс для UFW route."
|
||
fi
|
||
|
||
# Определяем реальный SSH-порт(ы), чтобы не отрезать доступ при нестандартном порту (Issue #91)
|
||
local ssh_ports _sp
|
||
ssh_ports=$(detect_ssh_ports)
|
||
log "SSH-порт(ы) для правила UFW: ${ssh_ports}"
|
||
|
||
# Смена порта при переустановке: удаляем правило старого порта до добавления
|
||
# нового, иначе старый UDP-порт остаётся открытым навсегда - единственный
|
||
# другой ufw delete живёт в uninstall и читает уже перезаписанный конфиг
|
||
# (Issue #175). SSH limit-правила сознательно не трогаем: авто-удаление
|
||
# SSH-правила при неверно определённом порте отрезало бы доступ к серверу.
|
||
if [[ -n "${PREV_AWG_PORT:-}" && "$PREV_AWG_PORT" =~ ^[0-9]+$ \
|
||
&& "$PREV_AWG_PORT" != "$AWG_PORT" ]]; then
|
||
if ufw delete allow "${PREV_AWG_PORT}/udp" >/dev/null 2>&1; then
|
||
log "UFW: правило старого порта ${PREV_AWG_PORT}/udp удалено (порт изменён на ${AWG_PORT})."
|
||
# Успех - снимаем отложенное удаление из awgsetup_cfg.init. При
|
||
# неудаче ключ остаётся: следующий запуск повторит попытку, а не
|
||
# потеряет её навсегда (PR #176).
|
||
sed -i '/^export PREV_AWG_PORT=/d' "$CONFIG_FILE" 2>/dev/null \
|
||
|| log_warn "Не удалось убрать PREV_AWG_PORT из $CONFIG_FILE."
|
||
PREV_AWG_PORT=""
|
||
else
|
||
log_warn "UFW: не удалось удалить правило старого порта ${PREV_AWG_PORT}/udp (возможно, правила нет). Попытка повторится при следующем запуске установки."
|
||
fi
|
||
fi
|
||
|
||
local ufw_errors=0
|
||
if ufw status 2>/dev/null | grep -q inactive; then
|
||
log "UFW неактивен. Настройка..."
|
||
ufw default deny incoming || { log_warn "UFW: ошибка default deny incoming"; ufw_errors=1; }
|
||
ufw default allow outgoing || { log_warn "UFW: ошибка default allow outgoing"; ufw_errors=1; }
|
||
for _sp in $ssh_ports; do
|
||
ufw limit "${_sp}/tcp" comment "SSH Rate Limit" || { log_warn "UFW: ошибка limit SSH (порт ${_sp})"; ufw_errors=1; }
|
||
done
|
||
ufw allow "${AWG_PORT}/udp" comment "AmneziaWG VPN" || { log_warn "UFW: ошибка allow VPN port"; ufw_errors=1; }
|
||
if [[ -n "$main_nic" ]]; then
|
||
ufw route allow in on awg0 out on "$main_nic" comment "AmneziaWG Routing" \
|
||
|| { log_warn "UFW: ошибка route rule"; ufw_errors=1; }
|
||
log "Правило маршрутизации VPN добавлено (awg0 → ${main_nic})."
|
||
fi
|
||
if [[ "$ufw_errors" -ne 0 ]]; then
|
||
log_error "Одна или несколько правил UFW не применились. Проверьте настройки вручную."
|
||
return 1
|
||
fi
|
||
log "Правила UFW добавлены."
|
||
log_warn "--- ВКЛЮЧЕНИЕ UFW ---"
|
||
log_warn "UFW разрешит SSH ТОЛЬКО на порту(ах): ${ssh_ports}. Убедитесь, что подключаетесь по нему."
|
||
if [[ "$ssh_ports" != "22" ]]; then
|
||
log_warn "ВНИМАНИЕ: SSH на нестандартном порту. Если порт определён неверно - доступ к серверу пропадёт."
|
||
log_warn "Override при необходимости: --ssh-port=ПОРТ"
|
||
fi
|
||
local confirm_ufw="y"
|
||
if [[ "$AUTO_YES" -eq 0 ]]; then
|
||
sleep 5
|
||
read -rp "Включить UFW? [y/N]: " confirm_ufw < /dev/tty
|
||
else
|
||
log "Автоматическое включение UFW (--yes)."
|
||
fi
|
||
if ! [[ "$confirm_ufw" =~ ^[[:space:]]*[Yy]([Ee][Ss])?[[:space:]]*$ ]]; then
|
||
log_warn "UFW настроен, но не активирован по вашему выбору."
|
||
log_warn "Сервер работает БЕЗ фаервола. Включить позже: sudo ufw enable"
|
||
return 0
|
||
fi
|
||
if ! ufw --force enable; then die "Ошибка включения UFW."; fi
|
||
log "UFW включен."
|
||
# Маркер: UFW был включён нашим установщиком (а не пользователем заранее).
|
||
# Используется в step_uninstall чтобы решить, безопасно ли отключать UFW.
|
||
# Защита от destructive uninstall на VPS где UFW использовался для SSH/web
|
||
# hardening ДО установки нашего скрипта (audit).
|
||
touch "$AWG_DIR/.ufw_enabled_by_installer" 2>/dev/null || \
|
||
log_warn "Не удалось создать UFW marker — uninstall не сможет отключить UFW автоматически."
|
||
else
|
||
log "UFW активен. Обновление правил..."
|
||
for _sp in $ssh_ports; do
|
||
ufw limit "${_sp}/tcp" comment "SSH Rate Limit" || { log_warn "UFW: ошибка limit SSH (порт ${_sp})"; ufw_errors=1; }
|
||
done
|
||
ufw allow "${AWG_PORT}/udp" comment "AmneziaWG VPN" || { log_warn "UFW: ошибка allow VPN port"; ufw_errors=1; }
|
||
if [[ -n "$main_nic" ]]; then
|
||
ufw route allow in on awg0 out on "$main_nic" comment "AmneziaWG Routing" \
|
||
|| { log_warn "UFW: ошибка route rule"; ufw_errors=1; }
|
||
fi
|
||
if [[ "$ufw_errors" -ne 0 ]]; then
|
||
log_error "Одна или несколько правил UFW не применились. Проверьте настройки вручную."
|
||
return 1
|
||
fi
|
||
ufw reload || log_warn "Ошибка перезагрузки UFW."
|
||
log "Правила обновлены."
|
||
fi
|
||
log "UFW настроен."
|
||
log "$(ufw status verbose 2>&1)"
|
||
return 0
|
||
}
|
||
|
||
secure_files() {
|
||
log "Установка безопасных прав доступа..."
|
||
chmod 700 "$AWG_DIR" 2>/dev/null
|
||
chmod 700 /etc/amnezia 2>/dev/null
|
||
chmod 700 /etc/amnezia/amneziawg 2>/dev/null
|
||
chmod 600 /etc/amnezia/amneziawg/*.conf 2>/dev/null
|
||
find "$AWG_DIR" -name "*.conf" -type f -exec chmod 600 {} \; 2>/dev/null
|
||
find "$AWG_DIR" -name "*.key" -type f -exec chmod 600 {} \; 2>/dev/null
|
||
find "$AWG_DIR" -name "*.png" -type f -exec chmod 600 {} \; 2>/dev/null
|
||
find "$AWG_DIR" -name "*.vpnuri" -type f -exec chmod 600 {} \; 2>/dev/null
|
||
if [[ -d "$KEYS_DIR" ]]; then
|
||
chmod 700 "$KEYS_DIR" 2>/dev/null
|
||
chmod 600 "$KEYS_DIR"/* 2>/dev/null
|
||
fi
|
||
[[ -f "$CONFIG_FILE" ]] && chmod 600 "$CONFIG_FILE"
|
||
[[ -f "$LOG_FILE" ]] && chmod 640 "$LOG_FILE"
|
||
[[ -f "$MANAGE_SCRIPT_PATH" ]] && chmod 700 "$MANAGE_SCRIPT_PATH"
|
||
[[ -f "$COMMON_SCRIPT_PATH" ]] && chmod 700 "$COMMON_SCRIPT_PATH"
|
||
log "Права доступа установлены."
|
||
}
|
||
|
||
setup_fail2ban() {
|
||
log "Настройка Fail2Ban..."
|
||
if ! command -v fail2ban-client &>/dev/null; then
|
||
install_packages fail2ban
|
||
# Маркер: пакет fail2ban доустановлен нашим установщиком (а не стоял
|
||
# до него). step_uninstall выполняет purge fail2ban только при наличии
|
||
# маркера, чтобы не снести SSH-защиту, настроенную пользователем заранее
|
||
# (симметрично .ufw_enabled_by_installer).
|
||
if command -v fail2ban-client &>/dev/null; then
|
||
touch "$AWG_DIR/.fail2ban_installed_by_installer" 2>/dev/null || \
|
||
log_warn "Не удалось создать fail2ban marker - uninstall не будет удалять пакет fail2ban."
|
||
fi
|
||
fi
|
||
if ! command -v fail2ban-client &>/dev/null; then
|
||
log_warn "Fail2ban не установлен, пропускаем."
|
||
return 1
|
||
fi
|
||
|
||
# banaction=ufw действует только при активном UFW: если на шаге 4
|
||
# пользователь отказался включать UFW, баны добавляются в неактивный
|
||
# набор правил и фактически не работают (fail2ban при этом "зелёный").
|
||
if ufw status 2>/dev/null | grep -q inactive; then
|
||
log_warn "UFW не активен: fail2ban-баны (banaction=ufw) не действуют, пока UFW выключен. Включить: sudo ufw enable"
|
||
fi
|
||
|
||
# Debian: journald вместо rsyslog, нужен python3-systemd
|
||
if [[ "${OS_ID:-}" == "debian" ]]; then
|
||
install_packages python3-systemd
|
||
fi
|
||
|
||
mkdir -p /etc/fail2ban/jail.d 2>/dev/null
|
||
|
||
# Backend: systemd для Debian и Ubuntu (нет rsyslog)
|
||
local f2b_backend="systemd"
|
||
|
||
cat > /etc/fail2ban/jail.d/amneziawg.conf << JAILEOF || { log_warn "Ошибка записи jail.d/amneziawg.conf"; return 1; }
|
||
# AmneziaWG — SSH protection (managed by amneziawg-installer)
|
||
[sshd]
|
||
enabled = true
|
||
backend = ${f2b_backend}
|
||
maxretry = 5
|
||
findtime = 10m
|
||
bantime = 1h
|
||
banaction = ufw
|
||
JAILEOF
|
||
|
||
systemctl restart fail2ban
|
||
# Одну секунду, сервис перезапускается...
|
||
sleep 1
|
||
|
||
if systemctl is-active --quiet fail2ban; then
|
||
log "Fail2Ban настроен и перезапущен."
|
||
else
|
||
log_warn "Ошибка перезапуска fail2ban"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Проверка статуса сервиса
|
||
# ==============================================================================
|
||
|
||
check_service_status() {
|
||
log "Проверка статуса сервиса..."
|
||
local ok=1
|
||
|
||
if systemctl is-failed --quiet awg-quick@awg0; then
|
||
log_error "Сервис FAILED!"
|
||
ok=0
|
||
fi
|
||
|
||
if ! ip addr show awg0 &>/dev/null; then
|
||
log_error "Интерфейс awg0 не найден!"
|
||
ok=0
|
||
fi
|
||
|
||
if ! awg show 2>/dev/null | grep -q "interface: awg0"; then
|
||
log_error "awg show не видит интерфейс!"
|
||
ok=0
|
||
fi
|
||
|
||
# Проверка порта
|
||
local port_check=${AWG_PORT:-0}
|
||
if [[ "$port_check" -eq 0 ]] && [[ -f "$CONFIG_FILE" ]]; then
|
||
# shellcheck source=/dev/null
|
||
port_check=$(safe_read_config_key "AWG_PORT" "$CONFIG_FILE")
|
||
port_check=${port_check:-0}
|
||
fi
|
||
if [[ "$port_check" -ne 0 ]]; then
|
||
if ! ss -lunp | grep -q ":${port_check} "; then
|
||
log_error "Порт $port_check/udp не прослушивается!"
|
||
ok=0
|
||
fi
|
||
fi
|
||
|
||
# Проверка AWG 2.0 параметров
|
||
if awg show awg0 2>/dev/null | grep -q "jc:"; then
|
||
log "AWG 2.0 параметры активны."
|
||
else
|
||
log_warn "AWG 2.0 параметры не обнаружены в awg show."
|
||
fi
|
||
|
||
if [[ "$ok" -eq 1 ]]; then
|
||
log "Статус сервиса и интерфейса OK."
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Диагностика
|
||
# ==============================================================================
|
||
|
||
# Отчёт собирают для вставки в ПУБЛИЧНЫЙ issue: наш же шаблон бага просит приложить
|
||
# его содержимое. Значения ключей вырезает одна общая функция, а не отдельная
|
||
# маскировка в каждой секции: секции со временем добавляются, и разовая маскировка
|
||
# внутри одной из них расходится с остальными. Именно так PresharedKey и оказался в
|
||
# отчёте открытым, пока в соседней строке маскировался PrivateKey.
|
||
# Применяется в ДВУХ точках (одна реализация, не два источника правды): на выходе
|
||
# всего отчёта и отдельно к серверному конфигу.
|
||
# ВАЖНО: второй рубеж закрывает ТОЛЬКО серверный конфиг. Вывод awg show и журнал
|
||
# через него не проходят, так что при отвязанном внешнем конвейере утекут именно они.
|
||
# ВАЖНО: маскировка AWG_ENDPOINT ниже по функции - осознанное исключение из правила
|
||
# "одна функция": она прячет адрес, а не ключ, и живёт своей строкой.
|
||
#
|
||
# Выражения КОНТЕКСТНЫЕ. Безъякорная редакция резала значение всюду, где встречалось
|
||
# имя ключа, и портила чужие поля: имя сервера - свободный текст, где разрешены
|
||
# пробелы и знак равенства, поэтому строка вида AWG_SERVER_NAME с текстом
|
||
# "PrivateKey = Office" внутри теряла и значение, и закрывающую кавычку. Имена
|
||
# клиентов от этого защищены валидацией ^[a-zA-Z0-9_-]+$ (она живёт в
|
||
# manage_amneziawg.sh и awg_common.sh, не здесь), но правленный руками #_Name может
|
||
# содержать что угодно.
|
||
#
|
||
# Четыре контекста:
|
||
# 1. строка конфигурации В НАЧАЛЕ СТРОКИ, с необязательным символом комментария.
|
||
# Он нужен НЕ потому, что awg разберёт такую строку: он её как раз отбрасывает
|
||
# целиком (config_read_line обрезает всё от первой решётки ДО разбора). Нужен
|
||
# потому, что значение физически лежит в файле, который вставят в публичный
|
||
# issue. Маскируем ДО КОНЦА СТРОКИ: разбор вычищает пробелы перед разбором,
|
||
# поэтому запись вида "PrivateKey = AA BB=" валидна, и обрезка по первому
|
||
# пробелу оставила бы хвост ключа в отчёте.
|
||
# 2. метка вывода awg show в начале строки. HeaderProtectionKey здесь обязателен:
|
||
# awg show печатает его ОТКРЫТЫМ ТЕКСТОМ (show.c: key() вместо masked_key()),
|
||
# в отличие от приватного и общего ключей, которые прячет сам. Заодно снимается
|
||
# вопрос про WG_HIDE_KEYS=never.
|
||
# 3. "Line unrecognized: ..." - без якоря. awg печатает в stderr строку УЖЕ
|
||
# ОЧИЩЕННУЮ (обрезана по решётке, пробелы удалены) и обёрнутую в обратный
|
||
# апостроф с закрывающей кавычкой. Отсюда ".?" в выражении: он пропускает этот
|
||
# апостроф. НЕ УДАЛЯТЬ ".?": без него настоящая строка не совпадает вовсе.
|
||
# Ветка error срабатывает на ЛЮБОЙ неопознанной строке: опечатка в имени ключа,
|
||
# ключ не в своей секции (PrivateKey внутри [Peer] уедет в stderr целиком),
|
||
# ключ чужой реализации. Дописанный руками параметр третьей линии - лишь один
|
||
# из случаев, а не единственный.
|
||
# 4. "Key is not the correct length or format: ..." - то же место, но имени ключа
|
||
# в сообщении НЕТ вовсе, поэтому по имени его не поймать.
|
||
# ВАЖНО: якоря у 1 и 2 означают, что эти формы НЕ ловятся в секции Service Status,
|
||
# где systemctl status добавляет свой префикс со штампом времени. Журнальная секция
|
||
# этим не страдает: journalctl вызывается там с --output=cat, то есть без префикса.
|
||
# Регистронезависимость (флаг I) - разбор конфига тоже регистронезависим (strncasecmp).
|
||
#
|
||
# ЧЕТЫРЕ СТРОКОВЫХ ЛИТЕРАЛА UPSTREAM, на которых всё держится: "private key:",
|
||
# "header protection key:", "Line unrecognized:", "Key is not the correct length or
|
||
# format:". Сверены с amneziawg-tools ee0f0a9 (src/config.c, src/show.c) 25 aug 2026.
|
||
# Переформулируют любой - фильтр молча перестанет совпадать, а тесты останутся
|
||
# зелёными, потому что зашивают те же строки. ПЕРЕСВЕРЯТЬ при обновлении tools.
|
||
_mask_report_secrets() {
|
||
sed -E \
|
||
-e 's/^([[:space:]]*#?[[:space:]]*(PrivateKey|PresharedKey|HeaderProtectionKey)[[:space:]]*=[[:space:]]*).*/\1[HIDDEN]/I' \
|
||
-e 's/^([[:space:]]*(private key|preshared key|header protection key)[[:space:]]*:[[:space:]]*).*/\1(hidden)/I' \
|
||
-e 's/(Line unrecognized:[[:space:]]*.?(PrivateKey|PresharedKey|HeaderProtectionKey)[[:space:]]*=[[:space:]]*).*/\1[HIDDEN]/I' \
|
||
-e 's/(Key is not the correct length or format:[[:space:]]*).*/\1[HIDDEN]/I'
|
||
}
|
||
|
||
create_diagnostic_report() {
|
||
# --diagnostic вызывается ДО initialize_setup (где живёт основной root-check):
|
||
# под обычным пользователем запись в /root/awg падает на каждом log_msg,
|
||
# отчёт не создаётся, а exit 0 выглядел бы как ложный успех.
|
||
if [ "$(id -u)" -ne 0 ]; then die "Запустите скрипт от root (sudo bash $0 --diagnostic)."; fi
|
||
log "Создание диагностики..."
|
||
local rf _diag_umask
|
||
rf="$AWG_DIR/diag_$(date +%F_%T).txt"
|
||
# Файл создаётся редиректом ДО chmod, поэтому режим на момент создания задаёт
|
||
# umask. Тот же приём, что для ключей в awg_common.sh: сузить права сразу, а не
|
||
# чинить после. --diagnostic отрабатывает ДО secure_files, поэтому /root/awg
|
||
# может быть создан с правами по умолчанию, и окно 0644 реально достижимо.
|
||
_diag_umask=$(umask); umask 077
|
||
{
|
||
echo "=== AMNEZIAWG 2.0 DIAGNOSTIC REPORT ==="
|
||
echo ""
|
||
echo "!!! ВНИМАНИЕ: значения PrivateKey, PresharedKey и HeaderProtectionKey"
|
||
echo "!!! вырезаны везде, где они подписаны своим именем или меткой awg show."
|
||
echo "!!! В отчёте остаются: IP-адреса, порты, маршруты, параметры обфускации,"
|
||
echo "!!! имена и публичные ключи клиентов. Адрес сервера дополнительно скрыт."
|
||
echo "!!! Перед публикацией в issue проверьте, что из этого вы не хотите раскрывать."
|
||
echo ""
|
||
echo "Generated: $(date)"
|
||
echo "Hostname: $(hostname)"
|
||
echo "Installer: v${SCRIPT_VERSION}"
|
||
echo ""
|
||
echo "--- OS ---"
|
||
lsb_release -ds 2>/dev/null || cat /etc/os-release
|
||
uname -a
|
||
echo ""
|
||
echo "--- Hardware ---"
|
||
echo "RAM: $(awk '/MemTotal/ {printf "%.0f MB", $2/1024}' /proc/meminfo)"
|
||
echo "CPU: $(nproc) cores"
|
||
echo "Swap: $(free -m | awk '/Swap:/ {print $2}') MB"
|
||
echo ""
|
||
echo "--- Configuration ($CONFIG_FILE) ---"
|
||
if [[ -f "$CONFIG_FILE" ]]; then
|
||
sed 's/AWG_ENDPOINT=.*/AWG_ENDPOINT=[HIDDEN]/' "$CONFIG_FILE"
|
||
else
|
||
echo "File not found"
|
||
fi
|
||
echo ""
|
||
echo "--- Server Config ($SERVER_CONF_FILE) ---"
|
||
# Второй рубеж ТОЙ ЖЕ функцией: одна реализация, две точки применения.
|
||
# Дубля правды это не создаёт, а если внешний фильтр когда-нибудь отвяжут
|
||
# от блока, самый опасный сырой ввод останется прикрытым.
|
||
if [[ -f "$SERVER_CONF_FILE" ]]; then
|
||
_mask_report_secrets < "$SERVER_CONF_FILE" || echo "ОШИБКА: не удалось прочитать или отфильтровать $SERVER_CONF_FILE"
|
||
else
|
||
echo "File not found"
|
||
fi
|
||
echo ""
|
||
echo "--- Service Status ---"
|
||
systemctl status awg-quick@awg0 --no-pager -l 2>/dev/null || echo "Service not found"
|
||
echo ""
|
||
echo "--- AWG Status ---"
|
||
awg show 2>/dev/null || echo "awg show failed"
|
||
echo ""
|
||
echo "--- AWG Version ---"
|
||
awg --version 2>/dev/null || echo "awg --version failed"
|
||
echo ""
|
||
echo "--- Network Interfaces ---"
|
||
ip a 2>/dev/null
|
||
echo ""
|
||
echo "--- Listening Ports ---"
|
||
ss -lunp 2>/dev/null
|
||
echo ""
|
||
echo "--- Firewall Status ---"
|
||
if command -v ufw &>/dev/null; then ufw status verbose; else echo "UFW N/A"; fi
|
||
echo ""
|
||
echo "--- Routing Table ---"
|
||
ip route 2>/dev/null
|
||
echo ""
|
||
echo "--- Cascade / Split Routing ---"
|
||
# Каскад (CASCADE.md) живёт вне awg0.conf: своя таблица, метка, ipset и правила mangle.
|
||
# Без этого блока по отчёту нельзя понять, применено деление или нет (issue #212).
|
||
if [ -f "$AWG_DIR/awg-routing.sh" ] || ip link show awg1 &>/dev/null; then
|
||
# is-active печатает "inactive" И возвращает ненулевой код, поэтому подстановка
|
||
# вида $(... || echo N/A) выдала бы ОБЕ строки сразу. Берём вывод, N/A - только на пустом.
|
||
local _casc_active _casc_enabled _casc_out
|
||
_casc_active=$(systemctl is-active awg-routing 2>/dev/null || true)
|
||
_casc_enabled=$(systemctl is-enabled awg-routing 2>/dev/null || true)
|
||
echo "unit awg-routing: active=${_casc_active:-N/A}, enabled=${_casc_enabled:-N/A}"
|
||
# Ниже намеренно разделены "не нашёл" и "не смог посмотреть": если гасить stderr и
|
||
# печатать одно и то же, отчёт превратит отказ команды в утверждение "правил нет",
|
||
# и разбор уйдёт не туда. grep -m10 вместо | head -10: он не рвёт пайп, поэтому
|
||
# pipefail не отдаёт 141 и ветка || не срабатывает после уже напечатанных строк.
|
||
if _casc_out=$(ip rule show 2>&1); then
|
||
grep -w fwmark <<< "$_casc_out" || echo "ip rule: правил по метке нет"
|
||
else
|
||
echo "ip rule: ПРОВЕРИТЬ НЕ УДАЛОСЬ: $(head -1 <<< "$_casc_out")"
|
||
fi
|
||
echo "table 100: $(ip route show table 100 2>/dev/null | tr '\n' '; ')"
|
||
echo "ipset sets: $(ipset list -n 2>/dev/null | tr '\n' ' ' || echo 'N/A')"
|
||
if _casc_out=$(ipset list ru 2>&1); then
|
||
grep "Number of entries" <<< "$_casc_out" || echo "ipset ru: счётчик не найден"
|
||
else
|
||
echo "ipset ru: набора нет ($(head -1 <<< "$_casc_out"))"
|
||
fi
|
||
echo "ru.zone: $(stat -c '%y, %s байт' "$AWG_DIR/ru.zone" 2>/dev/null || echo 'файла нет')"
|
||
if _casc_out=$(iptables -t mangle -S PREROUTING 2>&1); then
|
||
grep -m10 -E "match-set|MARK" <<< "$_casc_out" || echo "mangle PREROUTING: правил каскада нет"
|
||
else
|
||
echo "mangle PREROUTING: ПРОВЕРИТЬ НЕ УДАЛОСЬ: $(head -1 <<< "$_casc_out")"
|
||
fi
|
||
# Грепаем именно "-o awg1", а не MASQUERADE: обычное MASQUERADE на внешний интерфейс
|
||
# ставит сам установщик в PostUp, оно есть на ЛЮБОЙ установке, и по нему ветка "нет
|
||
# правил" была бы недостижима, а отчёт показывал бы NAT на месте при отсутствующем
|
||
# каскадном правиле. NAT проверяем обязательно: его скрипт ставит ПОСЛЕДНИМ, поэтому
|
||
# оборванный запуск оставляет всё остальное на месте, а его - нет. Симптом при этом
|
||
# обманчивый: российские сайты работают, остальное молчит, а отчёт без этой строки
|
||
# показывал бы полностью исправный каскад.
|
||
if _casc_out=$(iptables -t nat -S POSTROUTING 2>&1); then
|
||
grep -m10 -- "-o awg1" <<< "$_casc_out" || echo "nat POSTROUTING: правила каскада (-o awg1) нет"
|
||
else
|
||
echo "nat POSTROUTING: ПРОВЕРИТЬ НЕ УДАЛОСЬ: $(head -1 <<< "$_casc_out")"
|
||
fi
|
||
else
|
||
echo "не настроен"
|
||
fi
|
||
echo ""
|
||
echo "--- Kernel Params ---"
|
||
sysctl net.ipv4.ip_forward net.ipv6.conf.all.disable_ipv6 2>/dev/null
|
||
echo ""
|
||
echo "--- AWG Journal (last 50) ---"
|
||
journalctl -u awg-quick@awg0 -n 50 --no-pager --output=cat 2>/dev/null || echo "N/A"
|
||
echo ""
|
||
echo "--- Client List ---"
|
||
grep "^#_Name = " "$SERVER_CONF_FILE" 2>/dev/null | sed 's/^#_Name = //' || echo "N/A"
|
||
echo ""
|
||
echo "--- DKMS Status ---"
|
||
dkms status 2>/dev/null || echo "N/A"
|
||
echo ""
|
||
echo "--- Module Info ---"
|
||
modinfo amneziawg 2>/dev/null || echo "N/A"
|
||
echo ""
|
||
echo "=== END ==="
|
||
} | _mask_report_secrets > "$rf" || die "Ошибка записи отчета: $rf"
|
||
umask "$_diag_umask"
|
||
chmod 600 "$rf" || log_warn "Ошибка chmod отчета."
|
||
log "Отчет: $rf"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Деинсталляция
|
||
# ==============================================================================
|
||
|
||
step_uninstall() {
|
||
log "### ДЕИНСТАЛЛЯЦИЯ AMNEZIAWG ###"
|
||
echo ""
|
||
echo "ВНИМАНИЕ! Полное удаление AmneziaWG и конфигураций."
|
||
echo "Процесс необратим!"
|
||
echo ""
|
||
local confirm="" backup="Y"
|
||
if [[ "$AUTO_YES" -eq 0 ]]; then
|
||
read -rp "Уверены? (введите 'yes'): " confirm < /dev/tty
|
||
if [[ "$confirm" != "yes" ]]; then log "Деинсталляция отменена."; exit 1; fi
|
||
read -rp "Создать бэкап перед удалением? [Y/n]: " backup < /dev/tty
|
||
else
|
||
log "Автоматическое подтверждение деинсталляции (--yes)."
|
||
fi
|
||
if [[ -z "$backup" || "$backup" =~ ^[[:space:]]*[Yy]([Ee][Ss])?[[:space:]]*$ ]]; then
|
||
local bf
|
||
bf="$HOME/awg_uninstall_backup_$(date +%F_%H-%M-%S).tar.gz"
|
||
log "Создание бэкапа: $bf"
|
||
if tar -czf "$bf" -C / etc/amnezia "$AWG_DIR" --ignore-failed-read 2>/dev/null \
|
||
&& chmod 600 "$bf"; then
|
||
log "Бэкап создан: $bf"
|
||
else
|
||
log_warn "Бэкап не удался — проверьте $bf вручную перед продолжением"
|
||
fi
|
||
fi
|
||
# Загружаем флаг --no-tweaks из сохранённой конфигурации
|
||
local saved_no_tweaks=0
|
||
if [[ -f "$CONFIG_FILE" ]]; then
|
||
# shellcheck source=/dev/null
|
||
saved_no_tweaks=$(safe_read_config_key "NO_TWEAKS" "$CONFIG_FILE" 2>/dev/null) || saved_no_tweaks=0
|
||
saved_no_tweaks=${saved_no_tweaks:-0}
|
||
fi
|
||
log "Остановка сервиса..."
|
||
systemctl stop awg-quick@awg0 2>/dev/null
|
||
# Изоляционные DROP-правила (issue #178): PostDown конфига на диске мог
|
||
# уже не содержать -D DROP (прерванная переустановка on->off между шагами
|
||
# 6 и 7) - добираем stale-правила явно, как в шаге 7.
|
||
while iptables -D FORWARD -i awg0 -o awg0 -j DROP 2>/dev/null; do :; done
|
||
while ip6tables -D FORWARD -i awg0 -o awg0 -j DROP 2>/dev/null; do :; done
|
||
systemctl disable awg-quick@awg0 2>/dev/null
|
||
modprobe -r amneziawg 2>/dev/null || true
|
||
# v5.12.0+: автовосстановление модуля при обновлении ядра.
|
||
# Удаляем apt hook и systemd unit ДО apt purge, чтобы хук не сработал
|
||
# во время purge amneziawg-dkms (helper попытался бы пересобрать DKMS,
|
||
# но пакета уже нет). Файлы могут отсутствовать у установок до v5.12.0 —
|
||
# все операции idempotent.
|
||
log "Удаление компонентов автовосстановления модуля (v5.12.0+)..."
|
||
if systemctl is-enabled amneziawg-ensure-module.service &>/dev/null; then
|
||
systemctl disable amneziawg-ensure-module.service 2>/dev/null || true
|
||
fi
|
||
rm -f /etc/systemd/system/amneziawg-ensure-module.service \
|
||
/etc/apt/apt.conf.d/99-amneziawg-post-kernel \
|
||
/etc/logrotate.d/amneziawg-ensure-module \
|
||
/usr/local/sbin/amneziawg-ensure-module \
|
||
2>/dev/null
|
||
# Также подчищаем staging dotfiles, оставшиеся от прерванного install (atomic deploy).
|
||
rm -f /etc/systemd/system/.amneziawg-ensure-module.service.new \
|
||
/etc/apt/apt.conf.d/.99-amneziawg-post-kernel.new \
|
||
/etc/logrotate.d/.amneziawg-ensure-module.new \
|
||
/usr/local/sbin/.amneziawg-ensure-module.new \
|
||
2>/dev/null || true
|
||
rm -f /var/log/amneziawg-ensure-module.log* 2>/dev/null || true
|
||
rm -rf /var/lib/amneziawg 2>/dev/null || true
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
if [[ "$saved_no_tweaks" -eq 0 ]]; then
|
||
log "Очистка правил UFW для AmneziaWG..."
|
||
if command -v ufw &>/dev/null; then
|
||
local port_to_del
|
||
if [[ -f "$CONFIG_FILE" ]]; then
|
||
# shellcheck source=/dev/null
|
||
port_to_del=$(safe_read_config_key "AWG_PORT" "$CONFIG_FILE")
|
||
fi
|
||
port_to_del=${port_to_del:-39743}
|
||
# Удаление наших правил выполняется ВСЕГДА (idempotent)
|
||
ufw delete allow "${port_to_del}/udp" 2>/dev/null
|
||
# Для удаления route-правила нужно точное совпадение с тем как оно
|
||
# было создано: "ufw route allow in on awg0 out on <nic>". Без "out on"
|
||
# UFW не найдёт правило и оно останется в ufw status. Discussion #41.
|
||
local _nic
|
||
_nic=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1); exit}')
|
||
if [[ -n "$_nic" ]]; then
|
||
ufw route delete allow in on awg0 out on "$_nic" 2>/dev/null
|
||
fi
|
||
# Fallback: попытка удалить без out on (для совместимости со старыми правилами)
|
||
ufw route delete allow in on awg0 2>/dev/null
|
||
|
||
# ufw disable выполняется ТОЛЬКО если UFW был включён нашим установщиком.
|
||
# Защита от destructive uninstall на VPS где UFW использовался для
|
||
# SSH/web hardening ДО установки нашего скрипта (audit).
|
||
# Backwards compat: старые установки без маркера сохраняют UFW активным.
|
||
if [[ -f "$AWG_DIR/.ufw_enabled_by_installer" ]]; then
|
||
log "Отключение UFW (был включён нашим установщиком)..."
|
||
ufw --force disable 2>/dev/null
|
||
rm -f "$AWG_DIR/.ufw_enabled_by_installer"
|
||
else
|
||
log "UFW оставлен активным (использовался до установки или старая версия инсталлятора)."
|
||
fi
|
||
fi
|
||
log "Снятие блокировок Fail2Ban..."
|
||
if command -v fail2ban-client &>/dev/null; then
|
||
fail2ban-client unban --all 2>/dev/null || true
|
||
systemctl stop fail2ban 2>/dev/null
|
||
fi
|
||
else
|
||
log "Пропуск UFW/Fail2Ban (установка с --no-tweaks)."
|
||
fi
|
||
log "Удаление пакетов..."
|
||
# Снять hold с PPA-пакетов (ставился на пиновом пути H0) ДО удаления PPA:
|
||
# `apt-mark unhold` требует installed- или candidate-версию, а после удаления
|
||
# PPA (ниже) кандидат исчезает и apt-mark падает 'Can't select ... version',
|
||
# оставляя hold в dpkg-selection -> блокирует будущую переустановку. dpkg-
|
||
# fallback чистит selection напрямую, если PPA уже убран прошлым прогоном.
|
||
local _hp
|
||
for _hp in amneziawg amneziawg-dkms; do
|
||
apt-mark unhold "$_hp" >/dev/null 2>&1 || true
|
||
if dpkg --get-selections "$_hp" 2>/dev/null | grep -q '[[:space:]]hold$'; then
|
||
echo "$_hp deinstall" | dpkg --set-selections >/dev/null 2>&1 || true
|
||
fi
|
||
done
|
||
if [[ "$saved_no_tweaks" -eq 0 ]]; then
|
||
local _purge_pkgs=(amneziawg-dkms amneziawg-tools qrencode)
|
||
# fail2ban purge-им только если сами его доустановили (маркер из
|
||
# setup_fail2ban) - иначе пользовательская SSH-защита, стоявшая до
|
||
# установщика, не должна исчезать вместе с VPN. Наш jail-файл
|
||
# удаляется ниже в любом случае. Backwards compat: старые установки
|
||
# без маркера сохраняют fail2ban установленным.
|
||
if [[ -f "$AWG_DIR/.fail2ban_installed_by_installer" ]]; then
|
||
_purge_pkgs+=(fail2ban)
|
||
else
|
||
log "fail2ban оставлен установленным (стоял до установщика или старая версия инсталлятора)."
|
||
fi
|
||
DEBIAN_FRONTEND=noninteractive apt-get purge -y "${_purge_pkgs[@]}" 2>/dev/null || log_warn "Ошибка purge."
|
||
else
|
||
DEBIAN_FRONTEND=noninteractive apt-get purge -y amneziawg-dkms amneziawg-tools qrencode 2>/dev/null || log_warn "Ошибка purge."
|
||
fi
|
||
DEBIAN_FRONTEND=noninteractive apt-get autoremove -y 2>/dev/null || log_warn "Ошибка autoremove."
|
||
log "Удаление PPA и файлов..."
|
||
rm -f /etc/apt/sources.list.d/amnezia-ppa.sources \
|
||
/etc/apt/sources.list.d/amnezia-ppa.list \
|
||
/etc/apt/sources.list.d/amnezia-ubuntu-ppa-*.list \
|
||
/etc/apt/sources.list.d/amnezia-ubuntu-ppa-*.sources \
|
||
/etc/apt/keyrings/amnezia-ppa.gpg 2>/dev/null
|
||
rm -rf /etc/amnezia \
|
||
/etc/modules-load.d/amneziawg.conf \
|
||
/etc/sysctl.d/99-amneziawg-security.conf \
|
||
/etc/sysctl.d/99-amneziawg-forwarding.conf \
|
||
/etc/logrotate.d/amneziawg* || log_warn "Ошибка удаления файлов."
|
||
if [[ "$saved_no_tweaks" -eq 0 ]]; then
|
||
# Удаляем только наш собственный jail-файл.
|
||
# Раньше здесь была эвристика "если jail.local содержит banaction = ufw,
|
||
# удалить весь файл" — слишком широкий фильтр, мог снести чужой
|
||
# jail.local с custom jails. Эвристика убрана (audit).
|
||
# Если у юзера остался jail.local от очень старых версий нашего
|
||
# инсталлятора — пусть сам решает что с ним делать.
|
||
rm -f /etc/fail2ban/jail.d/amneziawg.conf 2>/dev/null
|
||
# Если fail2ban не purge-ился (стоял до нас) - перезапускаем его без
|
||
# нашего jail: выше он был остановлен (systemctl stop fail2ban).
|
||
if command -v fail2ban-client &>/dev/null && [[ ! -f "$AWG_DIR/.fail2ban_installed_by_installer" ]]; then
|
||
systemctl restart fail2ban 2>/dev/null || log_warn "Не удалось перезапустить fail2ban после удаления нашего jail."
|
||
fi
|
||
fi
|
||
log "Удаление DKMS..."
|
||
# Корректно снять DKMS-регистрацию (для всех версий amneziawg/*) и убрать
|
||
# исходник в /usr/src, а не только состояние в /var/lib/dkms. Hold с PPA-
|
||
# пакетов снят выше (до удаления PPA). `dkms status`: 'amneziawg/1.0.0, <kern>...'.
|
||
if command -v dkms >/dev/null 2>&1; then
|
||
local _dv
|
||
while IFS= read -r _dv; do
|
||
[[ -n "$_dv" ]] || continue
|
||
if ! dkms remove -m amneziawg -v "$_dv" --all >/dev/null 2>&1; then
|
||
log_warn "dkms remove amneziawg/$_dv не удался - дочищаю файлы вручную."
|
||
fi
|
||
done < <(dkms status 2>/dev/null | awk -F'[,/ ]+' '/^amneziawg[,/]/{print $2}' | sort -u)
|
||
fi
|
||
rm -rf /var/lib/dkms/amneziawg* /usr/src/amneziawg-* || log_warn "Ошибка удаления DKMS."
|
||
# Подчистить возможно оставшийся собранный .ko (если dkms remove не отработал) + depmod.
|
||
find /lib/modules -name 'amneziawg.ko*' -path '*/updates/dkms/*' -delete 2>/dev/null || true
|
||
command -v depmod >/dev/null 2>&1 && depmod -a >/dev/null 2>&1 || true
|
||
log "Восстановление sysctl..."
|
||
# Только точные строки, которые писали legacy-версии нашего инсталлятора
|
||
# (=1 для all/default/lo). Раньше удалялась ЛЮБАЯ строка с disable_ipv6 -
|
||
# включая добавленные самим пользователем (например =0 override).
|
||
if grep -qE '^net\.ipv6\.conf\.(all|default|lo)\.disable_ipv6[[:space:]]*=[[:space:]]*1[[:space:]]*$' /etc/sysctl.conf 2>/dev/null; then
|
||
sed -i -E '/^net\.ipv6\.conf\.(all|default|lo)\.disable_ipv6[[:space:]]*=[[:space:]]*1[[:space:]]*$/d' /etc/sysctl.conf || log_warn "Ошибка sed sysctl.conf"
|
||
fi
|
||
sysctl -p --system 2>/dev/null
|
||
rm -f /etc/apt/sources.list.d/*.bak-* "$AWG_DIR"/ubuntu.sources.bak-* 2>/dev/null || true
|
||
log "Удаление cron и скриптов..."
|
||
rm -f /etc/cron.d/awg-expiry 2>/dev/null
|
||
log "=== ДЕИНСТАЛЛЯЦИЯ ЗАВЕРШЕНА ==="
|
||
# Копируем лог и удаляем рабочую директорию
|
||
cp "$LOG_FILE" "$HOME/awg_uninstall.log" 2>/dev/null || true
|
||
rm -rf "$AWG_DIR" 2>/dev/null || true
|
||
exit 0
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 0: Инициализация
|
||
# ==============================================================================
|
||
|
||
initialize_setup() {
|
||
if [ "$(id -u)" -ne 0 ]; then die "Запустите скрипт от root (sudo bash $0)."; fi
|
||
|
||
mkdir -p "$AWG_DIR" || die "Ошибка создания $AWG_DIR"
|
||
chown root:root "$AWG_DIR"
|
||
|
||
# Process-wide lock: предотвращает запуск двух экземпляров install_amneziawg.sh
|
||
# одновременно. Без него два concurrent запуска могли бы прочитать одинаковый
|
||
# setup_state, конкурентно дёргать apt-get/dkms/ufw и сломать package state
|
||
# (audit).
|
||
# FD выбран фиксированным (9) и не конфликтует с update_state (использует 200).
|
||
# Lock держится открытым весь lifetime процесса — release автоматически на exit.
|
||
INSTALL_LOCK_FILE="$AWG_DIR/.install.lock"
|
||
exec 9>"$INSTALL_LOCK_FILE" || die "Не могу открыть $INSTALL_LOCK_FILE"
|
||
if ! flock -n 9; then
|
||
die "Другой экземпляр install_amneziawg.sh уже запущен. Подождите завершения, либо если процесс висит — удалите $INSTALL_LOCK_FILE и попробуйте снова."
|
||
fi
|
||
|
||
touch "$LOG_FILE" || die "Не удалось создать лог-файл $LOG_FILE"
|
||
chmod 640 "$LOG_FILE"
|
||
log "--- НАЧАЛО УСТАНОВКИ AmneziaWG 2.0 (v${SCRIPT_VERSION}) ---"
|
||
log "### ШАГ 0: Инициализация и проверка параметров ###"
|
||
cd "$AWG_DIR" || die "Ошибка перехода в $AWG_DIR"
|
||
log "Рабочая директория: $AWG_DIR"
|
||
log "Лог файл: $LOG_FILE"
|
||
|
||
check_os_version
|
||
check_container
|
||
check_kernel_version
|
||
check_free_space
|
||
|
||
local default_port=39743
|
||
local default_subnet="10.9.9.1/24"
|
||
local config_exists=0
|
||
|
||
# Инициализация переменных
|
||
AWG_PORT=$default_port
|
||
AWG_TUNNEL_SUBNET=$default_subnet
|
||
DISABLE_IPV6="default"
|
||
ALLOWED_IPS_MODE="default"
|
||
ALLOWED_IPS=""
|
||
AWG_ENDPOINT=""
|
||
CLIENT_ISOLATION=""
|
||
# Жёсткий сброс (не ${VAR:-}): внутренний ownership-маркер не должен
|
||
# наследоваться из окружения - экспортированная снаружи переменная иначе
|
||
# дотянется до удаления маршрутов из AllowedIPs (ревью PR #179).
|
||
CLIENT_ISOLATION_NET=""
|
||
|
||
# Жёсткий сброс до загрузки конфига: значение не должно приезжать из env.
|
||
AWG_SERVER_NAME=""
|
||
|
||
# Загрузка конфига
|
||
if [[ -f "$CONFIG_FILE" ]]; then
|
||
log "Найден файл конфигурации $CONFIG_FILE. Загрузка настроек..."
|
||
config_exists=1
|
||
# shellcheck source=/dev/null
|
||
safe_load_config "$CONFIG_FILE" || log_warn "Не удалось полностью загрузить настройки из $CONFIG_FILE."
|
||
AWG_PORT=${AWG_PORT:-$default_port}
|
||
AWG_TUNNEL_SUBNET=${AWG_TUNNEL_SUBNET:-$default_subnet}
|
||
DISABLE_IPV6=${DISABLE_IPV6:-"default"}
|
||
ALLOWED_IPS_MODE=${ALLOWED_IPS_MODE:-"default"}
|
||
ALLOWED_IPS=${ALLOWED_IPS:-""}
|
||
AWG_ENDPOINT=${AWG_ENDPOINT:-""}
|
||
# CLIENT_ISOLATION из конфига: строго 0|1 (whitelist-парсер значения не
|
||
# проверяет, а конфиг правят руками). Иначе арифметический контекст
|
||
# [[ "on" -eq 1 ]] разыменует строку как пустую переменную (=0) и молча
|
||
# ИНВЕРТИРУЕТ security-настройку: написал on - получил off (ревью PR #179).
|
||
case "${CLIENT_ISOLATION:-}" in
|
||
""|0|1) : ;;
|
||
*)
|
||
log_warn "CLIENT_ISOLATION='$CLIENT_ISOLATION' в $CONFIG_FILE не валиден (допустимо 0|1) - включаю изоляцию (безопасный дефолт)."
|
||
CLIENT_ISOLATION=1
|
||
;;
|
||
esac
|
||
# CLIENT_ISOLATION_NET - внутренний ownership-маркер: ровно один
|
||
# канонический IPv4 CIDR (вывод tunnel_network_cidr). Мусор с запятыми
|
||
# в substring-замене _apply_isolation_to_allowed_ips съел бы соседние
|
||
# пользовательские маршруты одной заменой (ревью PR #179).
|
||
if [[ -n "${CLIENT_ISOLATION_NET:-}" ]] \
|
||
&& [[ "$(tunnel_network_cidr "$CLIENT_ISOLATION_NET" || true)" != "$CLIENT_ISOLATION_NET" ]]; then
|
||
log_warn "CLIENT_ISOLATION_NET='$CLIENT_ISOLATION_NET' в $CONFIG_FILE не валиден (ожидается один канонический CIDR) - сбрасываю."
|
||
CLIENT_ISOLATION_NET=""
|
||
fi
|
||
log "Настройки из файла загружены."
|
||
else
|
||
log "Файл конфигурации $CONFIG_FILE не найден."
|
||
fi
|
||
|
||
# Старый порт из awgsetup_cfg.init: нужен шагу 4, чтобы удалить устаревшее
|
||
# UFW-правило при смене порта (Issue #175). Захват ДО CLI-override, иначе
|
||
# старое значение теряется навсегда - uninstall читает уже перезаписанный
|
||
# конфиг и старый порт не узнает. PREV_AWG_PORT мог уже загрузиться из
|
||
# awgsetup_cfg.init через safe_load_config - это отложенное удаление с
|
||
# прошлого запуска: шаг 1 завершается request_reboot, до шага 4 доживает
|
||
# только значение, записанное на диск (PR #176).
|
||
PREV_AWG_PORT="${PREV_AWG_PORT:-}"
|
||
_cfg_awg_port=""
|
||
if [[ "$config_exists" -eq 1 ]]; then _cfg_awg_port="$AWG_PORT"; fi
|
||
|
||
# Прежнее значение изоляции - для предупреждения о смене (issue #178).
|
||
# Legacy-конфиг без ключа = 1 (изолированно): иначе переход legacy -> --isolation=off не даёт предупреждения о regen.
|
||
_cfg_client_isolation=""
|
||
if [[ "$config_exists" -eq 1 ]]; then _cfg_client_isolation="${CLIENT_ISOLATION:-1}"; fi
|
||
|
||
# Прежнее имя сервера - для предупреждения о смене (D#180).
|
||
# Legacy-конфиг без ключа = 'AWG Server' (прежний хардкод).
|
||
_cfg_server_name=""
|
||
if [[ "$config_exists" -eq 1 ]]; then _cfg_server_name="${AWG_SERVER_NAME:-AWG Server}"; fi
|
||
|
||
# --mobile разворачивается в CLI_PRESET/CLI_PORT до их потребителей (g0vd).
|
||
resolve_mobile_flag
|
||
|
||
# Переопределение из CLI
|
||
AWG_PORT=${CLI_PORT:-$AWG_PORT}
|
||
# Порт изменился этим запуском - прежнее значение становится отложенным
|
||
# удалением. Если порт вернули обратно (совпал с отложенным) - удаление
|
||
# снимается: правило снова нужно.
|
||
if [[ -n "$_cfg_awg_port" && "$_cfg_awg_port" != "$AWG_PORT" ]]; then
|
||
PREV_AWG_PORT="$_cfg_awg_port"
|
||
fi
|
||
if [[ "$PREV_AWG_PORT" == "$AWG_PORT" ]]; then PREV_AWG_PORT=""; fi
|
||
AWG_TUNNEL_SUBNET=${CLI_SUBNET:-$AWG_TUNNEL_SUBNET}
|
||
if [[ "$CLI_DISABLE_IPV6" != "default" ]]; then DISABLE_IPV6=$CLI_DISABLE_IPV6; fi
|
||
if [[ "$CLI_ROUTING_MODE" != "default" ]]; then
|
||
ALLOWED_IPS_MODE=$CLI_ROUTING_MODE
|
||
# Явный CLI-режим вытесняет и список: раньше --route-all/--route-amnezia
|
||
# при переустановке меняли только режим, а ALLOWED_IPS оставался старым
|
||
# из awgsetup_cfg.init - флаг молча не действовал (Issue #170). Пустой
|
||
# список заставит configure_routing_mode пересчитать его под новый режим.
|
||
ALLOWED_IPS=""
|
||
# Ownership умирает вместе со списком, который он описывал: иначе
|
||
# stale CLIENT_ISOLATION_NET может присвоить себе токен пользователя
|
||
# из свежего --route-custom (issue #178).
|
||
CLIENT_ISOLATION_NET=""
|
||
if [[ "$CLI_ROUTING_MODE" -eq 3 ]]; then ALLOWED_IPS=$CLI_CUSTOM_ROUTES; fi
|
||
fi
|
||
if [[ -n "$CLI_ENDPOINT" ]]; then
|
||
if ! validate_endpoint "$CLI_ENDPOINT"; then
|
||
die "Некорректный --endpoint: '$CLI_ENDPOINT'. Допустимые форматы: FQDN (vpn.example.com), IPv4 (1.2.3.4), [IPv6] ([2001:db8::1]). Запрещены пробелы, табы, кавычки, обратный слеш и переводы строк."
|
||
fi
|
||
AWG_ENDPOINT=$CLI_ENDPOINT
|
||
fi
|
||
if [[ "$CLI_NO_TWEAKS" -eq 1 ]]; then NO_TWEAKS=1; fi
|
||
if [[ "$CLI_KEEP_PACKAGES" -eq 1 ]]; then KEEP_PACKAGES=1; fi
|
||
|
||
# Валидация после CLI override
|
||
validate_port "$AWG_PORT"
|
||
validate_subnet "$AWG_TUNNEL_SUBNET"
|
||
# AWG_ENDPOINT мог прийти из CONFIG_FILE через safe_load_config (без CLI override).
|
||
# Если значение есть и не валидно — log_warn + сброс в "" чтобы инсталлятор
|
||
# вернулся к auto-detect через get_server_public_ip (audit).
|
||
if [[ -n "$AWG_ENDPOINT" ]] && ! validate_endpoint "$AWG_ENDPOINT"; then
|
||
log_warn "AWG_ENDPOINT='$AWG_ENDPOINT' из $CONFIG_FILE не валиден, использую auto-detect."
|
||
AWG_ENDPOINT=""
|
||
fi
|
||
|
||
# Запрос у пользователя только на первом запуске
|
||
if [[ "$config_exists" -eq 0 ]]; then
|
||
log "Запрос настроек у пользователя (первый запуск)."
|
||
# Интерактивный ввод: опечатка не убивает установку (валидатор в
|
||
# subshell -> die печатает ошибку, но завершает только subshell, и
|
||
# запрос повторяется). Финальные validate_* вне цикла остаются
|
||
# авторитетными для CLI/конфиг-значений (там die уместен).
|
||
if [[ "$AUTO_YES" -eq 0 ]]; then
|
||
while true; do
|
||
read -rp "Введите UDP порт AmneziaWG (1-65535) [${AWG_PORT}]: " input_port < /dev/tty
|
||
[[ -z "$input_port" ]] && break
|
||
if ( validate_port "$input_port" ); then AWG_PORT=$input_port; break; fi
|
||
log_warn "Повторите ввод порта."
|
||
done
|
||
fi
|
||
validate_port "$AWG_PORT"
|
||
if [[ "$AUTO_YES" -eq 0 ]]; then
|
||
while true; do
|
||
read -rp "Введите подсеть туннеля [${AWG_TUNNEL_SUBNET}]: " input_subnet < /dev/tty
|
||
[[ -z "$input_subnet" ]] && break
|
||
if ( validate_subnet "$input_subnet" ); then AWG_TUNNEL_SUBNET=$input_subnet; break; fi
|
||
log_warn "Повторите ввод подсети."
|
||
done
|
||
fi
|
||
validate_subnet "$AWG_TUNNEL_SUBNET"
|
||
if [[ "$DISABLE_IPV6" == "default" ]]; then configure_ipv6; fi
|
||
if [[ "$ALLOWED_IPS_MODE" == "default" ]]; then configure_routing_mode; fi
|
||
else
|
||
log "Используются настройки из $CONFIG_FILE."
|
||
if [[ "$ALLOWED_IPS_MODE" == "3" ]] && [[ -n "$ALLOWED_IPS" ]]; then
|
||
if ! validate_cidr_list "$ALLOWED_IPS"; then
|
||
die "Некорректный ALLOWED_IPS в конфиге: '$ALLOWED_IPS'. Удалите $CONFIG_FILE и запустите установку заново."
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Согласие на удаление системных пакетов спрашиваем ВНЕ ветвления выше.
|
||
# Раньше вызов стоял только в ветке "конфига нет", и переустановка с --force на
|
||
# уже настроенном сервере проходила мимо вопроса: step99 удаляет файл состояния,
|
||
# поэтому повторный запуск начинается с шага 1 и снова доходит до очистки. У
|
||
# конфигов версий до 5.27.0 записи KEEP_PACKAGES нет вовсе, и она читалась как
|
||
# согласие - то есть issue #213 воспроизводился на версии, которая его чинит.
|
||
# Сама функция выходит сразу, если решение уже принято.
|
||
if [[ -n "$KEEP_PACKAGES" && "$KEEP_PACKAGES" != "0" && "$KEEP_PACKAGES" != "1" ]]; then
|
||
log_warn "В $CONFIG_FILE у KEEP_PACKAGES недопустимое значение '$KEEP_PACKAGES' - считаю, что пакеты надо сохранить."
|
||
KEEP_PACKAGES=1
|
||
fi
|
||
configure_package_cleanup
|
||
|
||
# Смена подсети при живых пирах запрещена - проверка до сохранения
|
||
# init-файла и любых изменений на диске (AWG_TUNNEL_SUBNET финален).
|
||
guard_subnet_change_with_peers
|
||
|
||
# Значения по умолчанию
|
||
if [[ "$DISABLE_IPV6" == "default" ]]; then DISABLE_IPV6=1; fi
|
||
configure_ipv6_tunnel
|
||
if [[ "$ALLOWED_IPS_MODE" == "default" ]]; then ALLOWED_IPS_MODE=2; fi
|
||
if [[ -z "$ALLOWED_IPS" ]]; then configure_routing_mode; fi
|
||
|
||
# Изоляция клиентов (issue #178): выбор + приведение AllowedIPs.
|
||
# Вызов до validate_cidr_list ниже - дописанная подсеть проходит ту же
|
||
# обязательную валидацию, что и остальной список.
|
||
configure_client_isolation
|
||
_apply_isolation_to_allowed_ips
|
||
|
||
# Единая обязательная валидация AllowedIPs до сохранения конфига: CLI
|
||
# --route-custom на первом запуске присваивал ALLOWED_IPS без проверки
|
||
# (configure_routing_mode пропускался, т.к. режим уже был 3). Проверяем
|
||
# любой непустой список независимо от источника (CLI / конфиг / выбор режима).
|
||
if [[ -n "$ALLOWED_IPS" ]] && ! validate_cidr_list "$ALLOWED_IPS"; then
|
||
die "Некорректный ALLOWED_IPS: '$ALLOWED_IPS'. Ожидается список x.x.x.x/y[,x.x.x.x/y]."
|
||
fi
|
||
|
||
# Имя сервера для vpn:// URI (D#180): выбор источника + валидация.
|
||
configure_server_name
|
||
|
||
# Проверка порта (пропускаем если AWG-сервис уже слушает этот порт)
|
||
if ! systemctl is-active --quiet awg-quick@awg0 2>/dev/null; then
|
||
check_port_availability "$AWG_PORT" || die "Порт $AWG_PORT/udp занят."
|
||
else
|
||
log "Сервис AWG активен — пропуск проверки порта."
|
||
fi
|
||
|
||
# Генерация AWG 2.0 параметров
|
||
# Перегенерация если: первый запуск ИЛИ явный CLI override (--preset/--jc/--jmin/--jmax)
|
||
if [[ -z "${AWG_Jc:-}" ]] || [[ -n "${CLI_PRESET:-}" ]] || [[ -n "${CLI_JC:-}" ]] \
|
||
|| [[ -n "${CLI_JMIN:-}" ]] || [[ -n "${CLI_JMAX:-}" ]]; then
|
||
# generate_awg_params перегенерирует ВЕСЬ набор (S1-S4, H1-H4, I1), а
|
||
# не только запрошенный параметр: при переустановке поверх живого
|
||
# сервера все выданные клиентские конфиги хранят старые H1-H4 и
|
||
# перестанут подключаться. Предупреждаем громко.
|
||
if [[ "$config_exists" -eq 1 && -n "${AWG_Jc:-}" ]]; then
|
||
log_warn "ВНИМАНИЕ: --preset/--jc/--jmin/--jmax при переустановке перегенерируют ВСЕ параметры обфускации (включая H1-H4/S1-S4/I1)."
|
||
log_warn "Все существующие клиентские конфиги перестанут подключаться - перевыпустите их после установки: sudo bash $MANAGE_SCRIPT_PATH regen"
|
||
fi
|
||
generate_awg_params
|
||
else
|
||
log "AWG 2.0 параметры уже заданы из конфига."
|
||
fi
|
||
|
||
# CPS (I1) toggle (issue #159): --no-cps убирает параметр I1, из-за которого
|
||
# десктопный AmneziaVPN на macOS виснет при подключении (мобильные и CLI-клиенты
|
||
# CPS понимают). Обнуляем ТОЛЬКО I1, остальной набор обфускации (Jc/S1-S4/H1-H4)
|
||
# не трогаем. Явные --preset/--jc/--jmin/--jmax без --no-cps возвращают CPS
|
||
# (свежая генерация набора включает I1). Иначе держим состояние из init.
|
||
if [[ "${CLI_NO_CPS:-0}" -eq 1 ]]; then
|
||
NO_CPS=1
|
||
elif [[ -n "${CLI_PRESET:-}" || -n "${CLI_JC:-}" || -n "${CLI_JMIN:-}" || -n "${CLI_JMAX:-}" ]]; then
|
||
NO_CPS=0
|
||
fi
|
||
if [[ "${NO_CPS:-0}" -eq 1 ]]; then
|
||
if [[ -n "${AWG_I1:-}" && "$config_exists" -eq 1 ]]; then
|
||
log_warn "ВНИМАНИЕ: --no-cps убирает параметр I1 (CPS). Существующие клиентские конфиги с I1 перестанут подключаться - перевыпустите их: sudo bash $MANAGE_SCRIPT_PATH regen"
|
||
fi
|
||
AWG_I1=''
|
||
log "CPS (I1) отключён (--no-cps / сохранённый NO_CPS=1): десктопный AmneziaVPN на macOS не поддерживает CPS."
|
||
fi
|
||
|
||
# Сохранение конфигурации
|
||
log "Сохранение настроек в $CONFIG_FILE..."
|
||
# temp в каталоге итогового конфига -> mv = атомарный rename на той же ФС
|
||
# (а не cross-fs copy+unlink, если /tmp смонтирован как tmpfs).
|
||
local temp_conf cfg_dir
|
||
cfg_dir="$(dirname "$CONFIG_FILE")"
|
||
mkdir -p "$cfg_dir" 2>/dev/null
|
||
temp_conf=$(mktemp -p "$cfg_dir") || die "Ошибка mktemp."
|
||
_install_temp_files+=("$temp_conf")
|
||
cat > "$temp_conf" << EOF
|
||
# Конфигурация установки AmneziaWG 2.0 (Авто-генерация)
|
||
# Используется скриптами установки и управления
|
||
export OS_ID='${OS_ID:-ubuntu}'
|
||
export OS_VERSION='${OS_VERSION:-}'
|
||
export OS_CODENAME='${OS_CODENAME:-}'
|
||
export AWG_PORT=${AWG_PORT}
|
||
export AWG_TUNNEL_SUBNET='${AWG_TUNNEL_SUBNET}'
|
||
export DISABLE_IPV6=${DISABLE_IPV6}
|
||
export ALLOWED_IPS_MODE=${ALLOWED_IPS_MODE}
|
||
export ALLOWED_IPS='${ALLOWED_IPS}'
|
||
export CLIENT_ISOLATION=${CLIENT_ISOLATION:-1}
|
||
export CLIENT_ISOLATION_NET='${CLIENT_ISOLATION_NET:-}'
|
||
export AWG_ENDPOINT='${AWG_ENDPOINT}'
|
||
export AWG_SERVER_NAME='${AWG_SERVER_NAME:-AWG Server}'
|
||
export AWG_MTU=${AWG_MTU:-1280}
|
||
# AWG 2.0 Parameters
|
||
export AWG_Jc=${AWG_Jc}
|
||
export AWG_Jmin=${AWG_Jmin}
|
||
export AWG_Jmax=${AWG_Jmax}
|
||
export AWG_S1=${AWG_S1}
|
||
export AWG_S2=${AWG_S2}
|
||
export AWG_S3=${AWG_S3}
|
||
export AWG_S4=${AWG_S4}
|
||
export AWG_H1='${AWG_H1}'
|
||
export AWG_H2='${AWG_H2}'
|
||
export AWG_H3='${AWG_H3}'
|
||
export AWG_H4='${AWG_H4}'
|
||
export AWG_I1='${AWG_I1}'
|
||
export AWG_I2='${AWG_I2:-}'
|
||
export AWG_I3='${AWG_I3:-}'
|
||
export AWG_I4='${AWG_I4:-}'
|
||
export AWG_I5='${AWG_I5:-}'
|
||
export AWG_PRESET='${AWG_PRESET:-default}'
|
||
export NO_TWEAKS=${NO_TWEAKS}
|
||
export KEEP_PACKAGES=${KEEP_PACKAGES:-1}
|
||
export NO_CPS=${NO_CPS}
|
||
export AWG_APPLY_MODE='${AWG_APPLY_MODE:-syncconf}'
|
||
export ALLOW_IPV6_TUNNEL=${ALLOW_IPV6_TUNNEL:-0}
|
||
export IPV6_SUBNET='${IPV6_SUBNET}'
|
||
export SERVER_HAS_NATIVE_IPV6=${SERVER_HAS_NATIVE_IPV6:-0}
|
||
EOF
|
||
# Отложенное удаление UFW-правила старого порта обязано пережить reboot:
|
||
# шаг 4 выполняется в другом процессе после 1-2 перезагрузок, переменная
|
||
# процесса до него не доживает (PR #176). Ключ снимает
|
||
# setup_improved_firewall после успешного ufw delete.
|
||
if [[ "$PREV_AWG_PORT" =~ ^[0-9]+$ ]]; then
|
||
echo "export PREV_AWG_PORT=${PREV_AWG_PORT}" >> "$temp_conf" \
|
||
|| die "Ошибка записи PREV_AWG_PORT в $temp_conf"
|
||
fi
|
||
if ! mv "$temp_conf" "$CONFIG_FILE"; then
|
||
rm -f "$temp_conf"
|
||
die "Ошибка сохранения $CONFIG_FILE"
|
||
fi
|
||
chmod 600 "$CONFIG_FILE" || log_warn "Ошибка chmod $CONFIG_FILE"
|
||
log "Настройки сохранены."
|
||
export AWG_PORT AWG_TUNNEL_SUBNET DISABLE_IPV6 ALLOWED_IPS_MODE ALLOWED_IPS AWG_ENDPOINT
|
||
log "Порт: ${AWG_PORT}/udp"
|
||
log "Подсеть: ${AWG_TUNNEL_SUBNET}"
|
||
log "Откл. IPv6: $DISABLE_IPV6"
|
||
log "Режим AllowedIPs: $ALLOWED_IPS_MODE"
|
||
log "Изоляция клиентов: $( [[ "${CLIENT_ISOLATION:-1}" -eq 1 ]] && echo включена || echo отключена )"
|
||
|
||
log "Имя сервера: ${AWG_SERVER_NAME}"
|
||
# Смена режима маршрутизации - операция над клиентскими конфигами: новые
|
||
# клиенты получат новый список, но у существующих regen сознательно
|
||
# сохраняет AllowedIPs (индивидуальные настройки modify). Подсказываем
|
||
# явный способ применить новый режим ко всем (Issue #170).
|
||
if [[ "$config_exists" -eq 1 && "$CLI_ROUTING_MODE" != "default" ]]; then
|
||
log_warn "Режим маршрутизации изменён. Существующие клиентские конфиги сохраняют старые AllowedIPs."
|
||
log_warn "Применить новый режим ко всем клиентам: sudo bash $MANAGE_SCRIPT_PATH regen --reset-routes"
|
||
fi
|
||
# Смена изоляции - та же операция над клиентскими конфигами, что и смена
|
||
# режима маршрутизации: новые клиенты получат новый список, существующие -
|
||
# только через regen --reset-routes (issue #178).
|
||
if [[ "$config_exists" -eq 1 \
|
||
&& "$_cfg_client_isolation" != "$CLIENT_ISOLATION" ]]; then
|
||
log_warn "Режим изоляции клиентов изменён. Существующие клиентские конфиги сохраняют старые AllowedIPs."
|
||
log_warn "Применить новый режим ко всем клиентам: sudo bash $MANAGE_SCRIPT_PATH regen --reset-routes"
|
||
fi
|
||
# Смена порта: шаг 6 пропускает уже существующих клиентов, их Endpoint
|
||
# остаётся со старым портом и они молча перестают подключаться. Подсказываем
|
||
# явный перевыпуск - по аналогии с предупреждением о смене режима (#170).
|
||
if [[ "$config_exists" -eq 1 && -n "$PREV_AWG_PORT" ]]; then
|
||
log_warn "Порт изменён (${PREV_AWG_PORT} -> ${AWG_PORT}). Существующие клиентские конфиги сохраняют старый порт в Endpoint и потеряют связь."
|
||
log_warn "Перевыпустить всех клиентов: sudo bash $MANAGE_SCRIPT_PATH regen"
|
||
fi
|
||
# Смена имени сервера (D#180): влияет только на vpn:// URI, существующие
|
||
# .vpnuri сохраняют старое имя до перевыпуска.
|
||
if [[ "$config_exists" -eq 1 && "$_cfg_server_name" != "$AWG_SERVER_NAME" ]]; then
|
||
log_warn "Имя сервера изменено ('${_cfg_server_name}' -> '${AWG_SERVER_NAME}'). Существующие vpn:// ссылки сохраняют старое имя."
|
||
log_warn "Перевыпустить с новым именем: sudo bash $MANAGE_SCRIPT_PATH regen"
|
||
fi
|
||
|
||
# Загрузка состояния
|
||
if [[ -f "$STATE_FILE" ]]; then
|
||
current_step=$(cat "$STATE_FILE")
|
||
if ! [[ "$current_step" =~ ^[0-9]+$ ]]; then
|
||
log_warn "$STATE_FILE поврежден."
|
||
current_step=1
|
||
update_state 1
|
||
else
|
||
log "Продолжение с шага $current_step."
|
||
fi
|
||
else
|
||
current_step=1
|
||
log "Начало с шага 1."
|
||
update_state 1
|
||
fi
|
||
|
||
# Stale state (прерванный шаг 7 оставляет setup_state=7/99) + CLI-параметры,
|
||
# влияющие на firewall/конфиги: без отката цикл пропустил бы шаги 4-6, новые
|
||
# значения остались бы только в awgsetup_cfg.init, а awg0.conf, клиентские
|
||
# конфиги и правила UFW продолжили бы жить со старыми - молча (Issue #175).
|
||
# Возврат к шагу 4: firewall (порт) + перегенерация конфигов (шаг 6).
|
||
if (( current_step > 4 )) && { [[ -n "$CLI_PORT" ]] || [[ -n "$CLI_SUBNET" ]] \
|
||
|| [[ -n "$CLI_SSH_PORT" ]] || [[ "$CLI_ROUTING_MODE" != "default" ]] \
|
||
|| [[ -n "$CLI_ENDPOINT" ]] || [[ "$CLI_DISABLE_IPV6" != "default" ]] \
|
||
|| [[ "${CLI_ALLOW_IPV6_TUNNEL:-0}" -eq 1 ]] || [[ -n "${CLI_PRESET:-}" ]] \
|
||
|| [[ -n "${CLI_JC:-}" ]] || [[ -n "${CLI_JMIN:-}" ]] || [[ -n "${CLI_JMAX:-}" ]] \
|
||
|| [[ "${CLI_ISOLATION:-default}" != "default" ]] \
|
||
|| [[ "${CLI_NO_CPS:-0}" -eq 1 ]]; }; then
|
||
log_warn "Незавершённая установка (шаг $current_step) + CLI-параметры конфигурации: возврат к шагу 4, чтобы firewall и конфиги были перегенерированы с новыми значениями."
|
||
current_step=4
|
||
update_state 4
|
||
fi
|
||
log "Шаг 0 завершен."
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 1: Обновление системы, очистка и оптимизация
|
||
# ==============================================================================
|
||
|
||
step1_update_and_optimize() {
|
||
update_state 1
|
||
log "### ШАГ 1: Обновление, очистка и оптимизация системы ###"
|
||
|
||
# Устойчивость к dpkg-lock на первой загрузке сервера: unattended-upgrades
|
||
# и apt-daily нередко держат блокировку несколько минут (issue #150 - тогда
|
||
# apt full-upgrade падал сразу). DPkg::Lock::Timeout заставляет apt дождаться
|
||
# освобождения блокировки, а не завершаться с ошибкой.
|
||
mkdir -p /etc/apt/apt.conf.d
|
||
printf 'DPkg::Lock::Timeout "300";\n' > /etc/apt/apt.conf.d/99-amneziawg-lock-timeout \
|
||
|| log_warn "Не удалось записать apt lock-timeout (митигация issue #150)."
|
||
|
||
# Очистка ненужных компонентов (ДО обновления для экономии трафика/времени)
|
||
if [[ "$NO_TWEAKS" -eq 1 ]]; then
|
||
log "Пропуск очистки системы (--no-tweaks)."
|
||
elif [[ "$KEEP_PACKAGES" != "0" ]]; then
|
||
# Не строгий ноль - значит либо явный отказ, либо решение неизвестно (пустое или
|
||
# испорченное значение из отредактированного руками конфига). Необратимое удаление
|
||
# делаем только по записанному согласию, всё остальное трактуем как "не трогать".
|
||
log "Пропуск очистки системы: пакеты сохраняются."
|
||
[[ -z "$KEEP_PACKAGES" ]] \
|
||
&& log_warn "Согласие на удаление системных пакетов не записано - ничего не удаляю."
|
||
else
|
||
cleanup_system
|
||
fi
|
||
|
||
log "Обновление списка пакетов..."
|
||
apt_update_tolerant || die "Ошибка apt update."
|
||
# Кэш свежий: install_packages ниже не должен гонять apt update повторно
|
||
# (источники в шаге 1 не меняются).
|
||
_APT_UPDATED=1
|
||
|
||
log "Разблокировка dpkg..."
|
||
if ! apt-get check &>/dev/null; then
|
||
log_warn "dpkg заблокирован или повреждён, исправление..."
|
||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a || log_warn "dpkg --configure -a."
|
||
fi
|
||
|
||
log "Обновление системы..."
|
||
if ! DEBIAN_FRONTEND=noninteractive apt full-upgrade -y; then
|
||
_lock_holder="$(fuser /var/lib/dpkg/lock-frontend 2>/dev/null | tr -s ' ' || true)"
|
||
if [[ -n "$_lock_holder" ]]; then
|
||
log_warn "dpkg-lock занят процессами:${_lock_holder} (обычно first-boot unattended-upgrades)."
|
||
fi
|
||
log_warn "apt full-upgrade не прошёл, чиню dpkg и повторяю..."
|
||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a || true
|
||
DEBIAN_FRONTEND=noninteractive apt full-upgrade -y \
|
||
|| die "Ошибка apt full-upgrade. Похоже, другой процесс apt/unattended-upgrades держит dpkg-lock. Дождитесь его завершения (проверить: fuser /var/lib/dpkg/lock-frontend) либо выполните: systemctl stop unattended-upgrades; dpkg --configure -a - и запустите скрипт снова."
|
||
fi
|
||
log "Система обновлена."
|
||
|
||
install_packages curl wget gpg sudo ethtool
|
||
|
||
if [[ "$NO_TWEAKS" -eq 0 ]]; then
|
||
# Оптимизация системы
|
||
optimize_system
|
||
# Настройка sysctl
|
||
setup_advanced_sysctl
|
||
else
|
||
log "Пропуск оптимизации и hardening (--no-tweaks)."
|
||
setup_minimal_sysctl
|
||
fi
|
||
|
||
log "Шаг 1 успешно завершен."
|
||
request_reboot 2
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Поддержка предсобранных пакетов для ARM
|
||
# ==============================================================================
|
||
|
||
# _try_install_prebuilt_arm — скачать и установить предсобранный .deb для
|
||
# текущего ARM-ядра из релиза arm-packages на GitHub.
|
||
#
|
||
# Возвращает 0 при успехе, 1 если совпадений нет или установка не удалась
|
||
# (в этом случае вызывающий код переходит к DKMS).
|
||
_try_install_prebuilt_arm() {
|
||
local kernel arch target_id asset_name asset_url tmpfile tmpsha expected_sha actual_sha
|
||
kernel="$(uname -r)"
|
||
arch="$(dpkg --print-architecture)"
|
||
|
||
if [[ "$kernel" == *+rpt-rpi-2712* ]]; then
|
||
target_id="rpi5-bookworm-arm64"
|
||
elif [[ "$kernel" == *+rpt* && "$arch" == "arm64" ]]; then
|
||
target_id="rpi-bookworm-arm64"
|
||
elif [[ "$kernel" == *+rpt* && "$arch" == "armhf" ]]; then
|
||
target_id="rpi-bookworm-armhf"
|
||
elif [[ "$kernel" == *-generic* && "${OS_VERSION:-}" == "24.04" ]]; then
|
||
target_id="ubuntu-2404-arm64"
|
||
elif [[ "$kernel" == *-generic* && "${OS_VERSION:-}" == "25.10" ]]; then
|
||
target_id="ubuntu-2510-arm64"
|
||
elif [[ "$kernel" == *-arm64* && "${OS_ID:-}" == "debian" && "${OS_VERSION:-}" == "13" ]]; then
|
||
target_id="debian-trixie-arm64"
|
||
elif [[ "$kernel" == *-arm64* && "${OS_ID:-}" == "debian" ]]; then
|
||
target_id="debian-bookworm-arm64"
|
||
else
|
||
log "Предсобранный пакет для ядра $kernel ($arch) не найден"
|
||
return 1
|
||
fi
|
||
|
||
asset_name="amneziawg-kmod-${target_id}_${kernel}_${arch}.deb"
|
||
asset_url="https://github.com/bivlked/amneziawg-installer/releases/download/arm-packages/${asset_name}"
|
||
|
||
log "Попытка установки предсобранного пакета: $asset_name"
|
||
tmpfile="$(mktemp /tmp/amneziawg-prebuilt-XXXXXX.deb)"
|
||
tmpsha="$(mktemp /tmp/amneziawg-prebuilt-XXXXXX.deb.sha256)"
|
||
|
||
# Сначала скачиваем контрольную сумму SHA256
|
||
if ! curl -fsSL --retry 2 --connect-timeout 10 --max-time 60 \
|
||
-o "$tmpsha" "${asset_url}.sha256" 2>/dev/null; then
|
||
log "Предсобранный пакет недоступен для $kernel — используется DKMS"
|
||
rm -f "$tmpfile" "$tmpsha"
|
||
return 1
|
||
fi
|
||
|
||
if curl -fsSL --retry 2 --connect-timeout 10 --max-time 60 \
|
||
-o "$tmpfile" "$asset_url" 2>/dev/null; then
|
||
# Проверяем целостность перед установкой модуля ядра
|
||
expected_sha="$(cat "$tmpsha")"
|
||
actual_sha="$(sha256sum "$tmpfile" | awk '{print $1}')"
|
||
rm -f "$tmpsha"
|
||
if [[ "$expected_sha" != "$actual_sha" ]]; then
|
||
log_warn "Несовпадение SHA256 предсобранного пакета — скачивание отклонено"
|
||
rm -f "$tmpfile"
|
||
return 1
|
||
fi
|
||
|
||
log "Пакет скачан (SHA256 OK), установка..."
|
||
if dpkg -i "$tmpfile" 2>/dev/null; then
|
||
rm -f "$tmpfile"
|
||
log "Предсобранный пакет установлен: $asset_name"
|
||
return 0
|
||
else
|
||
log_warn "Ошибка установки (несовпадение vermagic или повреждённый пакет)"
|
||
rm -f "$tmpfile"
|
||
return 1
|
||
fi
|
||
else
|
||
log "Предсобранный пакет недоступен для $kernel — используется DKMS"
|
||
rm -f "$tmpfile" "$tmpsha"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# H0 (AWG 3.0, 31 jul 2026): на ядрах < 6.7 актуальный PPA-модуль = AmneziaWG 3.0,
|
||
# и мы его туда сознательно не пускаем (почему именно - см. _kernel_supports_awg3).
|
||
# Устанавливаем ПИНОВЫЙ последний 2.0-модуль (линия 1.0.x) из исходника через DKMS:
|
||
# 1. git clone пинового тега --depth=1;
|
||
# 2. СВЕРКА commit с AWG2_PIN_COMMIT (integrity: immutable-коммит надёжнее, чем
|
||
# SHA авто-tarball GitHub, который меняется при смене компрессии);
|
||
# 3. upstream-механизм `make dkms-install` (кладёт в /usr/src/amneziawg-1.0.0);
|
||
# 4. dkms add/build/install под текущее ядро;
|
||
# 5. modprobe-проверка (собран != загружаем: Secure Boot может блокировать).
|
||
# dkms.conf исходника несёт AUTOINSTALL=yes, поэтому наш helper amneziawg-ensure-
|
||
# module (apt-hook + systemd) пересоберёт пиновый модуль при апгрейде ядра сам -
|
||
# отдельный maintenance-код не нужен. Возврат: 0 успех, 1 провал (лог в ERROR).
|
||
_install_pinned_awg2_module() {
|
||
local repo="https://github.com/amnezia-vpn/amneziawg-linux-kernel-module.git"
|
||
local kver work got_commit
|
||
local dkms_ver="1.0.0" # WIREGUARD_VERSION в upstream Makefile (имя /usr/src/amneziawg-<ver>)
|
||
kver="$(uname -r)"
|
||
|
||
if ! command -v git >/dev/null 2>&1; then
|
||
log_error "git не установлен - невозможно получить пиновый исходник модуля."
|
||
return 1
|
||
fi
|
||
|
||
work="$(mktemp -d /tmp/awg2-pin-XXXXXX)" || { log_error "mktemp -d не удался."; return 1; }
|
||
|
||
log "Клонирование пинового исходника AmneziaWG 2.0 ($AWG2_PIN_TAG)..."
|
||
if ! git clone --depth=1 --branch "$AWG2_PIN_TAG" "$repo" "$work/src" >/dev/null 2>&1; then
|
||
log_error "Не удалось клонировать $repo (тег $AWG2_PIN_TAG). Проверьте доступ к github.com."
|
||
rm -rf "$work"; return 1
|
||
fi
|
||
|
||
got_commit="$(git -C "$work/src" rev-parse HEAD 2>/dev/null || echo "")"
|
||
if [[ "$got_commit" != "$AWG2_PIN_COMMIT" ]]; then
|
||
log_error "Пин-проверка не пройдена: тег $AWG2_PIN_TAG -> commit '${got_commit:-<пусто>}',"
|
||
log_error "ожидался $AWG2_PIN_COMMIT. Отказ (возможна подмена/перемещение тега)."
|
||
rm -rf "$work"; return 1
|
||
fi
|
||
log "Пин-коммит подтверждён: $got_commit"
|
||
|
||
# Разложить DKMS-исходник upstream-механизмом (Makefile лежит в src/ подпапке).
|
||
# Вывод make сохраняем в лог: реальную причину сбоя (окружение/coreutils) иначе
|
||
# не увидеть - в отличие от dkms build, тут make.log не создаётся.
|
||
local _mklog="/var/log/amneziawg-pin-dkms-install.log"
|
||
if ! make -C "$work/src/src" dkms-install PREFIX=/usr >"$_mklog" 2>&1; then
|
||
log_error "make dkms-install не удался. Подробности: $_mklog"
|
||
rm -rf "$work"; return 1
|
||
fi
|
||
rm -rf "$work"
|
||
|
||
if [[ ! -f "/usr/src/amneziawg-${dkms_ver}/dkms.conf" ]]; then
|
||
log_error "/usr/src/amneziawg-${dkms_ver}/dkms.conf не появился после dkms-install."
|
||
return 1
|
||
fi
|
||
|
||
# add идемпотентен: при повторном запуске уже добавлен -> не фатально.
|
||
dkms add -m amneziawg -v "$dkms_ver" >/dev/null 2>&1 || true
|
||
# Идемпотентность (установщик - возобновляемая машина состояний): dkms build на
|
||
# уже собранном ядре возвращает ошибку "already built" -> собираем ТОЛЬКО если
|
||
# для этого ядра сборки ещё нет. install --force ниже идемпотентен сам по себе.
|
||
if dkms status -m amneziawg -v "$dkms_ver" -k "$kver" 2>/dev/null | grep -qE ': (built|installed)'; then
|
||
log "Пиновый 2.0-модуль уже собран для ядра $kver - пропускаю dkms build."
|
||
else
|
||
log "Сборка пинового 2.0-модуля через DKMS (ядро $kver)..."
|
||
if ! dkms build -m amneziawg -v "$dkms_ver" -k "$kver" >/dev/null 2>&1; then
|
||
log_error "DKMS build пинового 2.0-модуля не удался. Смотрите /var/lib/dkms/amneziawg/${dkms_ver}/${kver}/*/log/make.log"
|
||
return 1
|
||
fi
|
||
fi
|
||
if ! dkms install -m amneziawg -v "$dkms_ver" -k "$kver" --force >/dev/null 2>&1; then
|
||
log_error "DKMS install пинового 2.0-модуля не удался."
|
||
return 1
|
||
fi
|
||
|
||
# Собран != загружаем: при включённом Secure Boot неподписанный модуль не грузится.
|
||
if ! modprobe amneziawg 2>/dev/null; then
|
||
log_error "Модуль собран, но modprobe amneziawg не загрузил его."
|
||
log_error "Вероятная причина - Secure Boot: неподписанный DKMS-модуль блокируется."
|
||
log_error "Отключите Secure Boot в BIOS/UEFI VPS либо зарегистрируйте MOK-ключ."
|
||
return 1
|
||
fi
|
||
log "Пиновый AmneziaWG 2.0-модуль собран и загружен (DKMS $dkms_ver, ядро $kver)."
|
||
return 0
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 2: Установка AmneziaWG и зависимостей
|
||
# ==============================================================================
|
||
|
||
step2_install_amnezia() {
|
||
update_state 2
|
||
|
||
# Guard: убедиться что юзер действительно перезагрузился перед step 2.
|
||
# Если boot_id совпадает с сохранённым в request_reboot 2 — reboot
|
||
# не произошёл (например, юзер случайно запустил скрипт повторно).
|
||
# В этом случае apt full-upgrade из step 1 подложил новое ядро на диск,
|
||
# но работающее ядро всё ещё старое → DKMS соберёт модуль под старое,
|
||
# после следующего reboot modprobe упадёт.
|
||
local boot_id_file="$AWG_DIR/.boot_id_before_step2"
|
||
if [[ -f "$boot_id_file" ]] && [[ -r /proc/sys/kernel/random/boot_id ]]; then
|
||
local saved_boot_id current_boot_id
|
||
saved_boot_id=$(< "$boot_id_file")
|
||
current_boot_id=$(< /proc/sys/kernel/random/boot_id)
|
||
if [[ -n "$saved_boot_id" ]] && [[ "$saved_boot_id" == "$current_boot_id" ]]; then
|
||
die "Ожидалась перезагрузка перед шагом 2 (kernel upgrade активируется только после reboot). Выполните: sudo reboot — и запустите скрипт снова."
|
||
fi
|
||
log "Подтверждена перезагрузка (boot_id изменился) — продолжаем шаг 2"
|
||
rm -f "$boot_id_file" 2>/dev/null || true
|
||
fi
|
||
|
||
log "### ШАГ 2: Установка AmneziaWG и зависимостей ###"
|
||
_APT_UPDATED=0 # Reset: new sources will be added in this step
|
||
|
||
# --ppa-amnezia-tolerant ОБЯЗАТЕЛЕН уже здесь: если на диске остался
|
||
# PPA-файл с битым suite (404 Release; например questing от старой версии
|
||
# или после in-place upgrade), строгий update умирал ДО repair-блоков ниже
|
||
# и ремонт никогда не срабатывал (live-репро на Debian 12, v5.16.0 cycle).
|
||
# Ошибки базовых репозиториев по-прежнему fail-closed; PPA-ошибки чинит
|
||
# repair + post-PPA update + apt_wait_for_ppa_package ниже.
|
||
apt_update_tolerant --ppa-amnezia-tolerant || die "Ошибка apt update."
|
||
|
||
# PPA Amnezia (без software-properties-common)
|
||
log "Добавление PPA Amnezia..."
|
||
|
||
# Определение codename для PPA
|
||
# На Debian маппим на ближайший Ubuntu codename, т.к. PPA — это Launchpad (Ubuntu)
|
||
# Debian 12 (bookworm) → focal, Debian 13 (trixie) → noble
|
||
local codename ppa_codename
|
||
codename="${OS_CODENAME:-$(lsb_release -sc 2>/dev/null || echo "noble")}"
|
||
case "${OS_ID:-ubuntu}" in
|
||
debian)
|
||
case "$codename" in
|
||
bookworm) ppa_codename="focal" ;;
|
||
trixie) ppa_codename="noble" ;;
|
||
*) ppa_codename="noble" ;;
|
||
esac
|
||
log "Debian ($codename) → PPA codename: $ppa_codename"
|
||
;;
|
||
*)
|
||
ppa_codename="$codename"
|
||
# Для Ubuntu non-LTS (questing/plucky/oracular/...) PPA Amnezia
|
||
# пакетов не публикует — там 404 на dists/<codename>/Release.
|
||
# Проверяем доступность через HEAD-запрос и переключаемся на
|
||
# noble (LTS): сборка для noble корректно DKMS-собирается под
|
||
# текущее ядро.
|
||
# Связано: amnezia-vpn/amneziawg-linux-kernel-module#118
|
||
case "$ppa_codename" in
|
||
noble|jammy|focal)
|
||
# Known LTS — пропускаем pre-check (PPA точно опубликован)
|
||
;;
|
||
*)
|
||
log "Проверка доступности PPA Amnezia для Ubuntu '${ppa_codename}'..."
|
||
if ! curl -fsI --max-time 15 --retry 2 --retry-delay 5 \
|
||
"https://ppa.launchpadcontent.net/amnezia/ppa/ubuntu/dists/${ppa_codename}/Release" \
|
||
>/dev/null 2>&1; then
|
||
log_warn "PPA Amnezia не публикует пакеты для Ubuntu '${ppa_codename}' (HTTP 404 или host недоступен)."
|
||
log_warn "Переключаюсь на 'noble' — DKMS соберёт модуль под текущее ядро."
|
||
log_warn "Контекст: https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/118"
|
||
ppa_codename="noble"
|
||
else
|
||
log "PPA Amnezia доступен для '${ppa_codename}'."
|
||
fi
|
||
;;
|
||
esac
|
||
;;
|
||
esac
|
||
|
||
local keyring_dir="/etc/apt/keyrings"
|
||
local keyring_file="${keyring_dir}/amnezia-ppa.gpg"
|
||
local ppa_sources="/etc/apt/sources.list.d/amnezia-ppa.sources"
|
||
local ppa_list="/etc/apt/sources.list.d/amnezia-ppa.list"
|
||
# Проверка на legacy-файлы (от add-apt-repository предыдущих версий)
|
||
local legacy_list="/etc/apt/sources.list.d/amnezia-ubuntu-ppa-${codename}.list"
|
||
local legacy_sources="/etc/apt/sources.list.d/amnezia-ubuntu-ppa-${codename}.sources"
|
||
# Повторный запуск на сервере, где предыдущий (≤ v5.12.1) создал .sources
|
||
# с «битым» Suites=questing/plucky/etc.: если найденный suite не совпадает
|
||
# с целевым ppa_codename — удаляем файл, чтобы пересоздать ниже с правильным.
|
||
# Та же проверка для устаревшего .sources (формат от add-apt-repository).
|
||
# Если файл существует, но строка `Suites:` не парсится — считаем повреждённым
|
||
# и тоже пересоздаём, иначе сломанный файл проскочит как «PPA уже добавлен».
|
||
local existing_suite=""
|
||
if [[ -f "$ppa_sources" ]]; then
|
||
existing_suite=$(awk '/^Suites:/{print $2; exit}' "$ppa_sources" 2>/dev/null)
|
||
fi
|
||
if [[ -f "$ppa_sources" && ( -z "$existing_suite" || "$existing_suite" != "$ppa_codename" ) ]]; then
|
||
if [[ -z "$existing_suite" ]]; then
|
||
log_warn "$ppa_sources существует, но строка Suites: не найдена — пересоздание."
|
||
else
|
||
log_warn "Существующий PPA suite='${existing_suite}', целевой='${ppa_codename}' — пересоздание $ppa_sources."
|
||
fi
|
||
rm -f "$ppa_sources" "$ppa_list"
|
||
fi
|
||
local legacy_suite=""
|
||
if [[ -f "$legacy_sources" ]]; then
|
||
legacy_suite=$(awk '/^Suites:/{print $2; exit}' "$legacy_sources" 2>/dev/null)
|
||
fi
|
||
if [[ -f "$legacy_sources" && ( -z "$legacy_suite" || "$legacy_suite" != "$ppa_codename" ) ]]; then
|
||
log_warn "Устаревший PPA-файл $legacy_sources (suite='${legacy_suite:-<пусто>}') не соответствует целевому '${ppa_codename}' — удаление."
|
||
rm -f "$legacy_sources" "$legacy_list"
|
||
fi
|
||
# Тот же ремонт для traditional .list (Debian 12): suite - токен после URL
|
||
# в строке 'deb [opts] URL <suite> main'. Без проверки файл со старым/чужим
|
||
# suite (например после in-place upgrade bookworm->trixie) проскочил бы
|
||
# ниже как "PPA уже добавлен", и apt продолжил бы тянуть не тот suite.
|
||
local list_suite=""
|
||
if [[ -f "$ppa_list" ]]; then
|
||
list_suite=$(awk '/^deb([[:space:]]|$)/ {
|
||
for (i = 2; i <= NF; i++) {
|
||
if ($i ~ /^https?:/) { print $(i+1); exit }
|
||
}
|
||
}' "$ppa_list" 2>/dev/null)
|
||
if [[ -z "$list_suite" || "$list_suite" != "$ppa_codename" ]]; then
|
||
log_warn "Существующий $ppa_list (suite='${list_suite:-<пусто>}') не соответствует целевому '${ppa_codename}' - пересоздание."
|
||
rm -f "$ppa_list"
|
||
fi
|
||
fi
|
||
if [[ -f "$legacy_list" ]] || [[ -f "$legacy_sources" ]]; then
|
||
log "PPA уже добавлен (legacy-формат)."
|
||
elif [[ -f "$ppa_sources" ]] || [[ -f "$ppa_list" ]]; then
|
||
log "PPA уже добавлен."
|
||
else
|
||
mkdir -p "$keyring_dir"
|
||
log "Импорт GPG ключа Amnezia PPA..."
|
||
# Atomic: pipe в temp, затем mv — полу-записанный keyring никогда не
|
||
# окажется на целевом пути, даже если curl/gpg упали mid-way.
|
||
local _kf_tmp
|
||
_kf_tmp=$(mktemp -p "$keyring_dir" ".amnezia-ppa.gpg.tmp.XXXXXX") \
|
||
|| die "Не удалось создать временный файл для GPG ключа."
|
||
# --batch --no-tty --yes: gpg не открывает /dev/tty (non-interactive
|
||
# SSH, cloud-init, Ansible и т.п.) и не падает с "File exists" при
|
||
# overwrite mktemp-файла. Без этих флагов gpg в батч-режиме откажется
|
||
# писать в уже существующий пустой tmp-файл от mktemp.
|
||
# Запрос по ПОЛНОМУ 40-символьному fingerprint, не по короткому ID:
|
||
# для коротких 32-битных ID существуют preimage-коллизии (evil32), а
|
||
# keyserver.ubuntu.com принимает загрузку чужих ключей. Подменённый
|
||
# ключ не дал бы RCE (подпись пакетов не сойдётся), но ломал бы
|
||
# установку малопонятной ошибкой apt.
|
||
local _ppa_key_fpr="75C9DD72C799870E310542E24166F2C257290828"
|
||
if ! curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${_ppa_key_fpr}" \
|
||
| gpg --batch --no-tty --yes --dearmor -o "$_kf_tmp"; then
|
||
rm -f "$_kf_tmp" 2>/dev/null
|
||
die "Ошибка импорта GPG ключа Amnezia PPA."
|
||
fi
|
||
# Сверка fingerprint скачанного ключа с ожидаемым (pin).
|
||
local _got_fpr
|
||
_got_fpr=$(gpg --batch --no-tty --show-keys --with-colons "$_kf_tmp" 2>/dev/null \
|
||
| awk -F: '/^fpr:/{print $10; exit}')
|
||
if [[ "$_got_fpr" != "$_ppa_key_fpr" ]]; then
|
||
rm -f "$_kf_tmp" 2>/dev/null
|
||
die "GPG ключ Amnezia PPA не прошёл проверку fingerprint (получен: '${_got_fpr:-<пусто>}')."
|
||
fi
|
||
chmod 644 "$_kf_tmp" || { rm -f "$_kf_tmp" 2>/dev/null; die "Ошибка chmod GPG ключа."; }
|
||
mv -f "$_kf_tmp" "$keyring_file" \
|
||
|| { rm -f "$_kf_tmp" 2>/dev/null; die "Ошибка перемещения GPG ключа."; }
|
||
|
||
# Debian 12 использует traditional .list формат, Debian 13+ и Ubuntu 24.04+ — DEB822 .sources
|
||
if [[ "${OS_ID:-ubuntu}" == "debian" && "${OS_VERSION}" == "12" ]]; then
|
||
log "Debian 12: используем традиционный формат .list"
|
||
echo "deb [signed-by=${keyring_file}] https://ppa.launchpadcontent.net/amnezia/ppa/ubuntu ${ppa_codename} main" \
|
||
> "$ppa_list" || die "Ошибка создания $ppa_list"
|
||
chmod 644 "$ppa_list"
|
||
else
|
||
cat > "$ppa_sources" <<PPASRC || die "Ошибка создания sources PPA."
|
||
Types: deb
|
||
URIs: https://ppa.launchpadcontent.net/amnezia/ppa/ubuntu
|
||
Suites: ${ppa_codename}
|
||
Components: main
|
||
Signed-By: ${keyring_file}
|
||
PPASRC
|
||
chmod 644 "$ppa_sources"
|
||
fi
|
||
log "PPA добавлен."
|
||
fi
|
||
# apt-get update + классификация ошибок:
|
||
# - Ошибки ТОЛЬКО на PPA Amnezia → продолжаем, apt_wait_for_ppa_package
|
||
# ниже сделает retry (issue #68: ppa.launchpadcontent.net коротко лежит).
|
||
# - Любая другая non-source ошибка (DNS / GPG mismatch / dpkg lock на base
|
||
# mirror) → fall-fail. Продолжать на stale apt-cache небезопасно —
|
||
# следующий apt-get install упадёт с менее actionable сообщением
|
||
# (PR #69 review finding).
|
||
if ! apt_update_tolerant --ppa-amnezia-tolerant; then
|
||
log_error "apt-get update завершился с hard error — не PPA outage (issue #68)."
|
||
log_error "Проверьте: DNS, доступ к archive.ubuntu.com / deb.debian.org,"
|
||
log_error "целостность ключей в /etc/apt/keyrings, занятость dpkg lock."
|
||
die "apt update вернул ошибку (rc!=0, не PPA Amnezia)."
|
||
fi
|
||
# PPA добавлен, кэш обновлён: дальше в шаге 2 источники не меняются,
|
||
# поэтому install_packages не должен повторять apt update (на медленных
|
||
# зеркалах каждый прогон = 10-60 секунд).
|
||
_APT_UPDATED=1
|
||
# apt-get update толерантен к недоступному InRelease (rc=0 даже когда PPA
|
||
# лежит). Поэтому проверяем именно появление пакета amneziawg-dkms в
|
||
# apt-cache, с тремя попытками и backoff 30с/60с (≈1.5 мин total).
|
||
# Кратковременный outage ppa.launchpadcontent.net (issue #68) не должен
|
||
# валить установку.
|
||
if ! apt_wait_for_ppa_package amneziawg-dkms 3 30; then
|
||
log_error "Пакет amneziawg-dkms не появился в apt-cache после 3 попыток."
|
||
log_error "Похоже, ppa.launchpadcontent.net сейчас недоступен — это outage"
|
||
log_error "инфраструктуры Launchpad, не баг скрипта."
|
||
log_error "Подождите 10–15 минут и запустите скрипт снова той же командой."
|
||
log_error "Подробнее: https://github.com/bivlked/amneziawg-installer/issues/68"
|
||
die "PPA Amnezia временно недоступен."
|
||
fi
|
||
|
||
# Пакеты AmneziaWG + qrencode (БЕЗ Python!)
|
||
log "Установка пакетов AmneziaWG..."
|
||
|
||
# H0 (AWG 3.0, 31 jul 2026): путь пинового 2.0-модуля определяем ДО любой
|
||
# установки пакетов - hold обязан стоять раньше даже ARM-пути с предсобранным
|
||
# .deb, где install_packages ставит amneziawg-tools, чьи Recommends иначе
|
||
# подтянут 3.0-модуль в обход гейта. На ядрах < 6.7 PPA-модуль = AmneziaWG 3.0,
|
||
# его здесь не ставим (сознательно, см. _kernel_supports_awg3), а собираем
|
||
# пиновый 2.0 из исходника; из PPA берём только tools (version-aware, с 2.0
|
||
# работают - проверено).
|
||
local use_pinned_awg2=0
|
||
if ! _kernel_supports_awg3; then
|
||
use_pinned_awg2=1
|
||
log "Ядро $(uname -r) старее 6.7 - здесь ставим проверенный модуль AmneziaWG 2.0, а не 3.0 из PPA."
|
||
log "Активирован путь пинового AmneziaWG 2.0-модуля из исходника ($AWG2_PIN_TAG)."
|
||
# Re-entry: если прошлый прогон/стоковый установщик успел поставить (или
|
||
# оставить полу-настроенным) 3.0-пакет - убрать его и его исходник, иначе
|
||
# его failing postinst и владение /usr/src/amneziawg-* конфликтуют со сборкой.
|
||
if dpkg -l amneziawg-dkms 2>/dev/null | grep -qE '^(ii|iU|iF|iH|rc)'; then
|
||
log "Обнаружен ранее установленный amneziawg-dkms (AmneziaWG 3.0) - удаляю перед пиновой сборкой."
|
||
DEBIAN_FRONTEND=noninteractive apt-get purge -y amneziawg-dkms amneziawg >/dev/null 2>&1 \
|
||
|| dpkg --purge --force-all amneziawg-dkms amneziawg >/dev/null 2>&1 \
|
||
|| log_warn "Не удалось полностью удалить ранее установленный amneziawg-dkms - установка ниже может упасть."
|
||
command -v dkms >/dev/null 2>&1 && dkms remove -m amneziawg -v 1.0.0 --all >/dev/null 2>&1 || true
|
||
rm -rf /var/lib/dkms/amneziawg* /usr/src/amneziawg-* 2>/dev/null || true
|
||
fi
|
||
# ⚠️ Hold ДО любой установки: amneziawg-tools РЕКОМЕНДУЕТ amneziawg-dkms, apt
|
||
# по умолчанию ставит recommends -> без hold установка tools (в т.ч. на ARM-
|
||
# пути) притащила бы 3.0-dkms, и в системе оказались бы ДВА DKMS-дерева с
|
||
# одним именем модуля amneziawg - пиновое 2.0 и пакетное 3.0. Это защитный
|
||
# механизм, поэтому провал фатален (проверяем, что hold реально встал).
|
||
apt-mark hold amneziawg-dkms amneziawg >/dev/null 2>&1 || true
|
||
# Проверяем именно amneziawg-dkms - это несущий пакет: его РЕКОМЕНДУЕТ
|
||
# amneziawg-tools и он несёт 3.0-модуль. Метапакет amneziawg держать не
|
||
# обязательно (его Depends: amneziawg-dkms всё равно held), поэтому его
|
||
# отдельно не верифицируем.
|
||
if ! apt-mark showhold 2>/dev/null | grep -qx "amneziawg-dkms"; then
|
||
die "Не удалось зафиксировать amneziawg-dkms в hold. Без этого установка amneziawg-tools подтянет из PPA модуль AmneziaWG 3.0 в обход выбранного пути. Прервано (проверьте apt/dpkg lock)."
|
||
fi
|
||
else
|
||
# Ядро >= 6.7: нормальный путь ставит amneziawg-dkms из PPA. Снять возможный
|
||
# hold от прежнего пинового прогона (иначе apt install -y прервётся на held).
|
||
apt-mark unhold amneziawg-dkms amneziawg >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
# На ARM: сначала пробуем предсобранный .deb (не требует build-tools и headers).
|
||
# Откат на DKMS если совпадения нет или скачивание не удалось.
|
||
# ⚠️ На ядре < 6.7 (use_pinned_awg2=1) предсобранный .deb использовать БЕЗОПАСНО:
|
||
# наши ARM-пребилты собираются из scripts/arm-module-version.txt, запиненного на
|
||
# тот же 2.0-тег (v1.0.20260725) и залоченного тестом, то есть это ЗАВЕДОМО 2.0-
|
||
# модуль, а не 3.0. ⚠️ Это ЕДИНСТВЕННАЯ гарантия, и её достаточно. Прежний второй
|
||
# довод - "3.0 под ядро < 6.7 всё равно не скомпилируется, значит 3.0-ассета для
|
||
# debian-bookworm-arm64 в релизе быть не может" - с 31 jul 2026 НЕВЕРЕН (upstream
|
||
# починил сборку, v3.0.20260731-04), опираться на него нельзя. При отсутствии
|
||
# совпадения _try_install_prebuilt_arm вернёт 1 и мы уйдём в проверяемую сборку
|
||
# из исходника ниже. hold, выставленный выше, здесь тоже в силе (не даёт tools
|
||
# подтянуть 3.0-dkms через Recommends).
|
||
local arch
|
||
arch="$(uname -m)"
|
||
if [[ "$arch" == "aarch64" || "$arch" == "armv7l" ]]; then
|
||
if _try_install_prebuilt_arm; then
|
||
log "Модуль ядра установлен из предсобранного пакета. Установка утилит из PPA..."
|
||
# 🔴 Hold ОБЯЗАТЕЛЕН и здесь, НЕЗАВИСИМО от версии ядра. Выше он
|
||
# ставится только на пиновом пути (ядро < 6.7), а в ветке >= 6.7
|
||
# делается apt-mark unhold - и этот ARM-блок идёт ПОСЛЕ гейта.
|
||
# Предсобранный пакет называется amneziawg-kmod-<KERNEL_ID> и не
|
||
# объявляет Provides: amneziawg-modules, поэтому альтернативу из
|
||
# Recommends пакета amneziawg-tools (по живым метаданным PPA:
|
||
# "amneziawg-modules (>= 0.0.20171001) | amneziawg-dkms (>= ...)")
|
||
# он НЕ удовлетворяет, а самого amneziawg-modules в PPA нет.
|
||
# install_packages ставит через apt install -y С рекомендациями,
|
||
# значит без hold apt дотянул бы amneziawg-dkms из PPA, и рядом с
|
||
# нашим 2.0-модулем в extra/ встало бы 3.0-дерево в updates/dkms/.
|
||
# Два дерева с модулем ОДНОГО имени - ровно то, от чего hold нужен.
|
||
# Достижимо: target-ы пребилдов ubuntu-2510-arm64 и
|
||
# debian-trixie-arm64 - это ядра 6.7+.
|
||
apt-mark hold amneziawg-dkms amneziawg >/dev/null 2>&1 || true
|
||
if ! apt-mark showhold 2>/dev/null | grep -qx "amneziawg-dkms"; then
|
||
die "Не удалось зафиксировать amneziawg-dkms в hold перед установкой amneziawg-tools. Без этого рядом с предсобранным модулем встал бы модуль из PPA - два дерева с именем amneziawg. Проверьте apt/dpkg lock и запустите скрипт снова: предсобранный пакет уже установлен, шаг выполнится заново."
|
||
fi
|
||
install_packages "amneziawg-tools" "wireguard-tools" "qrencode"
|
||
# ПОСТУСЛОВИЕ. Выше проверена ПРЕДПОСЫЛКА (hold стоит), а результат
|
||
# не проверял никто - между ними apt мог поставить пакет по любой
|
||
# причине, которую мы не предусмотрели, либо dkms остался с прошлой
|
||
# установки этого же хоста. Проверяем факт, а не предпосылку: именно
|
||
# он ловит настоящий отказ.
|
||
# ⚠️ Здесь НЕ die, и это осознанно: сценарий "dkms остался с прошлого
|
||
# прогона" - это уже сломанное состояние, но обрывать установку на
|
||
# нём мы не готовы без прогона на ARM-стенде, а без пути к починке
|
||
# обрыв оставит человека ни с чем. Поэтому громкое предупреждение с
|
||
# точной командой. Ужесточение до die - отдельная задача.
|
||
if dpkg-query -W -f='${Status}' amneziawg-dkms 2>/dev/null | grep -q "ok installed"; then
|
||
log_warn "ВНИМАНИЕ: рядом с предсобранным модулем в системе стоит пакет amneziawg-dkms."
|
||
log_warn " Чем это кончится - измерено на стенде, а не предположено: как только в"
|
||
log_warn " системе окажутся kernel-headers, DKMS соберётся, ВЫТЕСНИТ файл пребилда из"
|
||
log_warn " extra/ и после перезагрузки загрузится он, то есть сервер молча перейдёт"
|
||
log_warn " на другую линию протокола. Туннель при этом работать не перестанет, а"
|
||
log_warn " dpkg продолжит считать предсобранный пакет установленным."
|
||
log_warn " Убрать лишнее: sudo apt-mark unhold amneziawg-dkms && sudo apt-get purge -y amneziawg-dkms"
|
||
log_warn " затем sudo apt-mark hold amneziawg-dkms, переустановка модуля и перезагрузка."
|
||
fi
|
||
log "Шаг 2 завершен (prebuilt ARM)."
|
||
# request_reboot всегда завершает процесс (exit), сюда не вернёмся.
|
||
request_reboot 3
|
||
fi
|
||
log "Совпадений не найдено — откат на DKMS."
|
||
fi
|
||
|
||
# Пакеты: на пиновом пути (ядро < 6.7) amneziawg-dkms НЕ ставим (это был бы
|
||
# 3.0-модуль), вместо него git для сборки пинового 2.0-исходника. Гейт, hold и
|
||
# очистка ранее установленного 3.0 выполнены выше (до ARM-блока).
|
||
local packages
|
||
if [[ "$use_pinned_awg2" -eq 1 ]]; then
|
||
packages=("amneziawg-tools" "wireguard-tools" "dkms"
|
||
"build-essential" "dpkg-dev" "git" "qrencode")
|
||
else
|
||
packages=("amneziawg-dkms" "amneziawg-tools" "wireguard-tools" "dkms"
|
||
"build-essential" "dpkg-dev" "qrencode")
|
||
fi
|
||
|
||
# Linux headers: на Debian может не быть точного linux-headers-$(uname -r)
|
||
local current_headers
|
||
current_headers="linux-headers-$(uname -r)"
|
||
if dpkg -s "$current_headers" &>/dev/null || apt-cache show "$current_headers" &>/dev/null 2>&1; then
|
||
packages+=("$current_headers")
|
||
else
|
||
log_warn "Нет headers для $(uname -r), установка общего пакета..."
|
||
local kernel_release
|
||
kernel_release="$(uname -r)"
|
||
if [[ "$kernel_release" == *+rpt* || "$kernel_release" == *-rpi* ]]; then
|
||
# Ядро Raspberry Pi Foundation (+rpt suffix) — использовать мета-пакет RPi
|
||
# linux-headers-rpi-2712: Pi 5 / Cortex-A76; linux-headers-rpi-v8: Pi 3/4 arm64
|
||
local rpi_headers
|
||
if [[ "$kernel_release" == *2712* ]]; then
|
||
rpi_headers="linux-headers-rpi-2712"
|
||
else
|
||
rpi_headers="linux-headers-rpi-v8"
|
||
fi
|
||
log "Обнаружено ядро Raspberry Pi, используем $rpi_headers"
|
||
packages+=("$rpi_headers")
|
||
elif [[ "${OS_ID:-ubuntu}" == "debian" ]]; then
|
||
# На Debian: linux-headers-$(dpkg --print-architecture)
|
||
local arch_pkg
|
||
arch_pkg="linux-headers-$(dpkg --print-architecture 2>/dev/null || echo "amd64")"
|
||
packages+=("$arch_pkg")
|
||
else
|
||
packages+=("linux-headers-generic")
|
||
fi
|
||
fi
|
||
# v5.13.0: на 25.10/26.04 после in-place upgrade с 24.04 в системе могут
|
||
# остаться kernel headers от 24.04 (6.8.x), скомпилированные gcc-13. В
|
||
# 25.10 по умолчанию ставится только gcc-15 → dkms autoinstall в postinst
|
||
# пакета amneziawg-dkms падает при сборке под устаревшие ядра, и dpkg
|
||
# оставляет amneziawg* unconfigured. Если в системе обнаружены kernel
|
||
# headers, отличные от running, заранее доставляем gcc-13 (доступен в
|
||
# questing/universe и 26.04 archive), чтобы autoinstall прошёл для всех
|
||
# ядер.
|
||
local _running_kernel _has_stale=0 _hd _hd_kern
|
||
_running_kernel="$(uname -r)"
|
||
for _hd in /lib/modules/*/build; do
|
||
[[ -e "$_hd" ]] || continue
|
||
_hd_kern="${_hd#/lib/modules/}"
|
||
_hd_kern="${_hd_kern%/build}"
|
||
if [[ "$_hd_kern" != "$_running_kernel" ]]; then
|
||
_has_stale=1
|
||
break
|
||
fi
|
||
done
|
||
if [[ "$_has_stale" -eq 1 ]] && ! command -v gcc-13 >/dev/null 2>&1; then
|
||
if apt-cache madison gcc-13 2>/dev/null | grep -q .; then
|
||
log "Обнаружены устаревшие kernel headers (≠ $_running_kernel) — устанавливаю gcc-13 для совместимости DKMS autoinstall."
|
||
DEBIAN_FRONTEND=noninteractive apt install -y gcc-13 \
|
||
|| log_warn "Не удалось установить gcc-13 — DKMS autoinstall может падать на устаревших ядрах."
|
||
else
|
||
log_warn "Обнаружены устаревшие kernel headers, но gcc-13 недоступен в repo — DKMS autoinstall может падать."
|
||
fi
|
||
fi
|
||
install_packages "${packages[@]}"
|
||
|
||
# H0: пиновый путь - собрать 2.0-модуль из исходника ВМЕСТО PPA-amneziawg-dkms.
|
||
# Headers текущего ядра уже установлены выше (в packages); hold выставлен ранее.
|
||
if [[ "$use_pinned_awg2" -eq 1 ]]; then
|
||
if ! _install_pinned_awg2_module; then
|
||
log_error "Не удалось установить пиновый AmneziaWG 2.0-модуль."
|
||
log_error "На ядрах старее 6.7 (у вас $(uname -r)) установщик берёт модуль не из"
|
||
log_error "PPA, а собирает из исходника, и этот шаг не прошёл. Конкретная причина -"
|
||
log_error "в строках выше; чаще всего это отсутствующие kernel headers, нехватка"
|
||
log_error "места, оборванная сеть или собранный, но не загружаемый модуль (Secure"
|
||
log_error "Boot). Запасной вариант: развернуть сервер на Ubuntu 24.04/25.10 или"
|
||
log_error "Debian 13, где модуль приходит пакетом из PPA. См. README/INSTALL_VPS."
|
||
die "Пиновый AmneziaWG 2.0-модуль не установлен."
|
||
fi
|
||
log "Пиновый AmneziaWG 2.0-модуль установлен; PPA-dkms в hold (защита от 3.0)."
|
||
fi
|
||
|
||
# v5.12.0: мета-пакет linux-headers, чтобы apt автоматически подтягивал
|
||
# заголовки при kernel upgrade. Без меты ставится только
|
||
# linux-headers-$(uname -r) — он не tracking новые ядра, и на следующем
|
||
# apt upgrade DKMS-модуль не успеет пересобраться.
|
||
#
|
||
# Detect kernel flavor (Ubuntu cloud images: aws/azure/gcp/oracle/kvm/
|
||
# lowlatency/raspi; Debian cloud-amd64) — обычный linux-headers-generic
|
||
# на Azure-VM tracking не тот kernel-pipeline. Берём суффикс uname -r,
|
||
# пробуем flavor-specific meta, fallback на generic/arch.
|
||
local arch_meta kernel_rel
|
||
arch_meta="$(dpkg --print-architecture 2>/dev/null || echo '')"
|
||
kernel_rel="$(uname -r)"
|
||
local -a meta_candidates=()
|
||
if [[ "$kernel_rel" == *+rpt* || "$kernel_rel" == *-rpi* ]]; then
|
||
: # RPi: мета linux-headers-rpi-{2712,v8} уже добавлена в packages выше.
|
||
elif [[ "${OS_ID:-ubuntu}" == "ubuntu" ]]; then
|
||
# Ubuntu uname -r формат: 6.8.0-49-generic / 6.8.0-1009-aws / ...
|
||
local flavor="${kernel_rel##*-}"
|
||
if [[ -n "$flavor" && "$flavor" != "$kernel_rel" ]]; then
|
||
meta_candidates+=("linux-headers-${flavor}")
|
||
fi
|
||
meta_candidates+=("linux-headers-generic")
|
||
elif [[ "${OS_ID:-}" == "debian" && -n "$arch_meta" ]]; then
|
||
# Debian: обычное ядро 6.12.85+deb13-amd64, cloud — 6.12.85+deb13-cloud-amd64.
|
||
[[ "$kernel_rel" == *-cloud-* ]] \
|
||
&& meta_candidates+=("linux-headers-cloud-${arch_meta}")
|
||
meta_candidates+=("linux-headers-${arch_meta}")
|
||
fi
|
||
local meta meta_installed=0
|
||
for meta in "${meta_candidates[@]}"; do
|
||
if dpkg-query -W -f='${Status}' "$meta" 2>/dev/null \
|
||
| grep -q 'install ok installed'; then
|
||
log "$meta уже установлен (auto-tracking ядерных обновлений)."
|
||
meta_installed=1
|
||
break
|
||
fi
|
||
log "Установка мета-пакета $meta..."
|
||
if DEBIAN_FRONTEND=noninteractive apt install -y "$meta" 2>/dev/null; then
|
||
log "$meta установлен."
|
||
meta_installed=1
|
||
break
|
||
fi
|
||
log_warn "Не удалось установить $meta — пробуем следующий вариант."
|
||
done
|
||
if [[ ${#meta_candidates[@]} -gt 0 && $meta_installed -eq 0 ]]; then
|
||
log_warn "Ни один meta-пакет kernel-headers не установлен — auto-rebuild при kernel upgrade может не работать."
|
||
fi
|
||
|
||
# v5.12.0: standalone helper /usr/local/sbin/amneziawg-ensure-module
|
||
# вызывается из apt hook (DPkg::Post-Invoke) и из Phase 4 systemd-юнита.
|
||
# Helper самодостаточен — не source awg_common.sh, чтобы оставаться
|
||
# рабочим даже после ручного перемещения /root/awg/.
|
||
#
|
||
# Развёртывание делается через staging-файл в той же FS, что и target,
|
||
# с финальным `mv -f` — гарантирует atomic-подмену (cross-FS rename
|
||
# = copy+remove, НЕ atomic). Стейджинг-файл начинается с точки —
|
||
# apt и logrotate пропускают dotfiles при сканировании каталога.
|
||
log "Развёртывание helper'а DKMS auto-repair..."
|
||
mkdir -p /usr/local/sbin
|
||
local _stage_helper=/usr/local/sbin/.amneziawg-ensure-module.new
|
||
cat > "$_stage_helper" <<'AWG_ENSURE_HELPER_EOF'
|
||
#!/bin/bash
|
||
# amneziawg-ensure-module — rebuilds the AmneziaWG DKMS module after a
|
||
# kernel upgrade.
|
||
#
|
||
# Generated by install_amneziawg.sh (v5.12.0+). Do not edit; re-run the
|
||
# installer to refresh.
|
||
#
|
||
# Modes:
|
||
# --hook — invoked from /etc/apt/apt.conf.d/99-amneziawg-post-kernel
|
||
# (DPkg::Post-Invoke). Constraints:
|
||
# - MUST NOT call apt-get install: the parent apt still
|
||
# holds /var/lib/dpkg/lock-frontend, a nested install
|
||
# would deadlock.
|
||
# - Skips modprobe and systemctl: the running kernel may
|
||
# still be the old one. The newly-built module is
|
||
# loaded after reboot via the systemd unit, or via
|
||
# `manage repair-module`.
|
||
# Stamp-file fast-path keeps routine apt ops noise-free.
|
||
#
|
||
# --systemd — invoked from amneziawg-ensure-module.service at boot,
|
||
# ordered Before=awg-quick@awg0.service. Builds for every
|
||
# target kernel (same as --hook), then loads the module
|
||
# via modprobe so awg-quick can start. No stamp fast-path
|
||
# — boot must always verify load state, even if /lib/modules
|
||
# hasn't changed since the last build (module not loaded
|
||
# across reboots). Exit 1 if modprobe fails so systemd
|
||
# marks the unit as failed (visible via systemctl status).
|
||
#
|
||
# Iteration target: every kernel that exposes /lib/modules/<ver>/build
|
||
# (= a directory with installed headers). uname -r alone is insufficient
|
||
# in apt-hook context because it returns the OLD running kernel while
|
||
# the new kernel's headers are already on disk.
|
||
#
|
||
# Output: stdout / stderr; --hook appends to
|
||
# /var/log/amneziawg-ensure-module.log (rotated weekly via
|
||
# /etc/logrotate.d/amneziawg-ensure-module). --systemd writes to journal
|
||
# (StandardOutput=journal, StandardError=journal in the unit file).
|
||
|
||
set -euo pipefail
|
||
|
||
MODE="${1:-}"
|
||
case "$MODE" in
|
||
--hook|--systemd) ;;
|
||
--help|-h) echo "Usage: $0 --hook | --systemd"; exit 0 ;;
|
||
*) echo "amneziawg-ensure-module: missing or unknown mode (use --hook or --systemd)" >&2; exit 2 ;;
|
||
esac
|
||
|
||
ts() { date '+%Y-%m-%d %H:%M:%S'; }
|
||
log_line() { printf '[%s] [%s] %s\n' "$(ts)" "$MODE" "$*"; }
|
||
|
||
if [[ $(id -u) -ne 0 ]]; then
|
||
log_line "ERROR: root privileges required" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if ! command -v dkms >/dev/null 2>&1; then
|
||
log_line "WARN: dkms is not installed — nothing to do"
|
||
exit 0
|
||
fi
|
||
|
||
declare -a target_kernels=()
|
||
shopt -s nullglob
|
||
for build_dir in /lib/modules/*/build; do
|
||
[[ -d "$build_dir" || -L "$build_dir" ]] || continue
|
||
target_kernels+=("$(basename "$(dirname "$build_dir")")")
|
||
done
|
||
shopt -u nullglob
|
||
|
||
if [[ ${#target_kernels[@]} -eq 0 ]]; then
|
||
log_line "WARN: no /lib/modules/*/build directories — kernel headers missing"
|
||
exit 0
|
||
fi
|
||
|
||
# Build per-run state signature (mtime + kver) used by both modes:
|
||
# --hook — for stamp-file fast-path comparison (silent exit if equal)
|
||
# --systemd — recorded after success so subsequent --hook calls can skip
|
||
STAMP_DIR=/var/lib/amneziawg
|
||
STAMP_FILE="${STAMP_DIR}/ensure-module.stamp"
|
||
current_state=""
|
||
for kver in "${target_kernels[@]}"; do
|
||
# stat may fail (build dir removed in flight) — guard against set -e abort.
|
||
# Empty mtime → comparison differs → we re-run dkms autoinstall (acceptable).
|
||
mtime="$(stat -c '%Y' "/lib/modules/${kver}/build" 2>/dev/null || true)"
|
||
current_state+="${mtime} ${kver} "
|
||
done
|
||
|
||
# Fast-path applies ONLY to --hook. Boot (--systemd) must always run the
|
||
# full path — module is not loaded across reboots even when /lib/modules
|
||
# state is unchanged.
|
||
if [[ "$MODE" == "--hook" ]] \
|
||
&& [[ -f "$STAMP_FILE" && "$(cat "$STAMP_FILE" 2>/dev/null)" == "$current_state" ]]; then
|
||
# Silent exit — routine apt ops don't add log noise.
|
||
exit 0
|
||
fi
|
||
|
||
# Strip the deprecated REMAKE_INITRD directive (triggers noisy warnings
|
||
# on modern DKMS releases).
|
||
for cfg in /var/lib/dkms/amneziawg/*/source/dkms.conf; do
|
||
[[ -f "$cfg" ]] && sed -i '/^REMAKE_INITRD=/d' "$cfg" 2>/dev/null || true
|
||
done
|
||
|
||
build_rc=0
|
||
for kver in "${target_kernels[@]}"; do
|
||
log_line "dkms autoinstall -k $kver"
|
||
if ! dkms autoinstall -k "$kver"; then
|
||
log_line "WARN: dkms autoinstall failed for kernel $kver" >&2
|
||
build_rc=1
|
||
fi
|
||
done
|
||
|
||
depmod -a 2>/dev/null || true
|
||
|
||
# --systemd: load the module so awg-quick can start. Exit 1 on modprobe
|
||
# failure — systemd marks the unit failed; visible via `systemctl status
|
||
# amneziawg-ensure-module.service`. awg-quick still starts (Before= is
|
||
# ordering only, not a dependency) and surfaces its own error if the
|
||
# module is unavailable.
|
||
if [[ "$MODE" == "--systemd" ]]; then
|
||
log_line "modprobe amneziawg"
|
||
if ! modprobe amneziawg 2>&1; then
|
||
log_line "ERROR: modprobe amneziawg failed for running kernel $(uname -r)" >&2
|
||
log_line " Check: /var/lib/dkms/amneziawg/<ver>/<kernel>/log/make.log" >&2
|
||
exit 1
|
||
fi
|
||
if ! lsmod 2>/dev/null | grep -q '^amneziawg '; then
|
||
log_line "ERROR: amneziawg module not present in lsmod after modprobe" >&2
|
||
exit 1
|
||
fi
|
||
log_line "amneziawg module loaded for $(uname -r)"
|
||
# Update stamp on --systemd success (current kernel is usable, what matters
|
||
# for boot) even if some other kernel's build failed (build_rc=1).
|
||
mkdir -p "$STAMP_DIR" 2>/dev/null || true
|
||
printf '%s' "$current_state" > "$STAMP_FILE" 2>/dev/null || true
|
||
log_line "done"
|
||
exit 0
|
||
fi
|
||
|
||
# --hook: update stamp only on full success — partial failures retry next run.
|
||
if [[ $build_rc -eq 0 ]]; then
|
||
mkdir -p "$STAMP_DIR" 2>/dev/null || true
|
||
printf '%s' "$current_state" > "$STAMP_FILE" 2>/dev/null || true
|
||
fi
|
||
|
||
log_line "done (rc=$build_rc)"
|
||
exit "$build_rc"
|
||
AWG_ENSURE_HELPER_EOF
|
||
chown root:root "$_stage_helper" 2>/dev/null || true
|
||
chmod 0755 "$_stage_helper" \
|
||
|| { rm -f "$_stage_helper"; die "Не удалось chmod helper'а."; }
|
||
mv -f "$_stage_helper" /usr/local/sbin/amneziawg-ensure-module \
|
||
|| { rm -f "$_stage_helper"; die "Не удалось развернуть helper amneziawg-ensure-module."; }
|
||
log "Helper /usr/local/sbin/amneziawg-ensure-module развёрнут."
|
||
|
||
# v5.12.0: apt hook DPkg::Post-Invoke вызывает helper после kernel upgrade.
|
||
mkdir -p /etc/apt/apt.conf.d
|
||
local _stage_hook=/etc/apt/apt.conf.d/.99-amneziawg-post-kernel.new
|
||
cat > "$_stage_hook" <<'AWG_APT_HOOK_EOF'
|
||
// amneziawg-installer (v5.12.0+): rebuild DKMS module after kernel upgrades.
|
||
// Generated by install_amneziawg.sh — do not edit; re-run the installer to refresh.
|
||
DPkg::Post-Invoke {"if [ -x /usr/local/sbin/amneziawg-ensure-module ]; then /usr/local/sbin/amneziawg-ensure-module --hook >>/var/log/amneziawg-ensure-module.log 2>&1 || true; fi";};
|
||
AWG_APT_HOOK_EOF
|
||
chown root:root "$_stage_hook" 2>/dev/null || true
|
||
chmod 0644 "$_stage_hook" \
|
||
|| { rm -f "$_stage_hook"; die "Не удалось chmod apt-hook."; }
|
||
mv -f "$_stage_hook" /etc/apt/apt.conf.d/99-amneziawg-post-kernel \
|
||
|| { rm -f "$_stage_hook"; die "Не удалось развернуть apt-hook."; }
|
||
log "Apt-hook 99-amneziawg-post-kernel установлен (auto-rebuild при apt upgrade ядра)."
|
||
|
||
# v5.12.0: logrotate для /var/log/amneziawg-ensure-module.log
|
||
mkdir -p /etc/logrotate.d
|
||
local _stage_logrotate=/etc/logrotate.d/.amneziawg-ensure-module.new
|
||
cat > "$_stage_logrotate" <<'AWG_LOGROTATE_EOF'
|
||
/var/log/amneziawg-ensure-module.log {
|
||
weekly
|
||
rotate 4
|
||
compress
|
||
delaycompress
|
||
missingok
|
||
notifempty
|
||
copytruncate
|
||
}
|
||
AWG_LOGROTATE_EOF
|
||
chown root:root "$_stage_logrotate" 2>/dev/null || true
|
||
chmod 0644 "$_stage_logrotate" \
|
||
|| { rm -f "$_stage_logrotate"; die "Не удалось chmod logrotate-конфиг."; }
|
||
mv -f "$_stage_logrotate" /etc/logrotate.d/amneziawg-ensure-module \
|
||
|| { rm -f "$_stage_logrotate"; die "Не удалось развернуть logrotate-конфиг."; }
|
||
log "Logrotate-конфиг /etc/logrotate.d/amneziawg-ensure-module установлен (weekly, rotate 4)."
|
||
|
||
# v5.12.0 Phase 4: systemd unit гарантирует, что модуль ядра построен
|
||
# и загружен ДО старта awg-quick@awg0 на каждом boot. Type=oneshot +
|
||
# RemainAfterExit=yes + Before=awg-quick@awg0.service — стандартный
|
||
# pre-load pattern (после kernel upgrade DKMS пересборка может
|
||
# понадобиться сразу при первом boot нового ядра).
|
||
log "Развёртывание systemd-юнита amneziawg-ensure-module.service..."
|
||
mkdir -p /etc/systemd/system
|
||
local _stage_unit=/etc/systemd/system/.amneziawg-ensure-module.service.new
|
||
cat > "$_stage_unit" <<'AWG_SYSTEMD_UNIT_EOF'
|
||
[Unit]
|
||
Description=Ensure amneziawg kernel module is built and loaded
|
||
Documentation=https://github.com/bivlked/amneziawg-installer
|
||
Before=awg-quick@awg0.service
|
||
After=systemd-modules-load.service local-fs.target
|
||
ConditionPathExists=/usr/local/sbin/amneziawg-ensure-module
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
ExecStart=/usr/local/sbin/amneziawg-ensure-module --systemd
|
||
TimeoutStartSec=300
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
AWG_SYSTEMD_UNIT_EOF
|
||
chown root:root "$_stage_unit" 2>/dev/null || true
|
||
chmod 0644 "$_stage_unit" \
|
||
|| { rm -f "$_stage_unit"; die "Не удалось chmod systemd unit."; }
|
||
mv -f "$_stage_unit" /etc/systemd/system/amneziawg-ensure-module.service \
|
||
|| { rm -f "$_stage_unit"; die "Не удалось развернуть systemd unit."; }
|
||
if ! systemctl daemon-reload; then
|
||
log_warn "systemctl daemon-reload завершился с ошибкой — unit может не активироваться до перезагрузки."
|
||
fi
|
||
if ! systemctl enable amneziawg-ensure-module.service; then
|
||
log_warn "Не удалось enable amneziawg-ensure-module.service — boot-time auto-rebuild не будет срабатывать."
|
||
fi
|
||
log "Systemd-юнит amneziawg-ensure-module.service установлен и enabled (Before=awg-quick@awg0)."
|
||
|
||
# DKMS статус
|
||
log "Проверка статуса DKMS..."
|
||
local dkms_stat
|
||
dkms_stat=$(dkms status 2>&1)
|
||
if ! echo "$dkms_stat" | grep -q 'amneziawg.*installed'; then
|
||
log_warn "DKMS статус не OK."
|
||
log_msg "WARN" "$dkms_stat"
|
||
else
|
||
log "DKMS статус OK."
|
||
fi
|
||
|
||
log "Шаг 2 завершен."
|
||
request_reboot 3
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 3: Проверка модуля ядра
|
||
# ==============================================================================
|
||
|
||
step3_check_module() {
|
||
update_state 3
|
||
log "### ШАГ 3: Проверка модуля ядра ###"
|
||
sleep 2
|
||
|
||
if ! lsmod | grep -q -w amneziawg; then
|
||
log "Модуль не загружен. Загрузка..."
|
||
modprobe amneziawg || die "Ошибка modprobe amneziawg."
|
||
log "Модуль загружен."
|
||
local mf="/etc/modules-load.d/amneziawg.conf"
|
||
mkdir -p "$(dirname "$mf")"
|
||
if ! grep -qxF 'amneziawg' "$mf" 2>/dev/null; then
|
||
echo "amneziawg" > "$mf" || log_warn "Ошибка записи $mf"
|
||
log "Добавлено в $mf."
|
||
fi
|
||
else
|
||
log "Модуль amneziawg загружен."
|
||
fi
|
||
|
||
log "Информация о модуле:"
|
||
modinfo amneziawg | grep -E "filename|version|vermagic|srcversion" | while IFS= read -r line; do
|
||
log " $line"
|
||
done
|
||
|
||
local cv kr
|
||
cv=$(modinfo amneziawg 2>/dev/null | awk '/^vermagic:/{print $2}')
|
||
if [[ -z "$cv" ]]; then
|
||
die "Не удалось прочитать vermagic модуля amneziawg. Проверьте: modprobe amneziawg && modinfo amneziawg"
|
||
fi
|
||
kr=$(uname -r)
|
||
if [[ "$cv" != "$kr" ]]; then
|
||
log_warn "VerMagic НЕ совпадает: Модуль($cv) != Ядро($kr)!"
|
||
else
|
||
log "VerMagic совпадает."
|
||
fi
|
||
|
||
# Проверка версии awg
|
||
if command -v awg &>/dev/null; then
|
||
local awg_ver
|
||
awg_ver=$(awg --version 2>/dev/null || echo "неизвестна")
|
||
log "Версия awg: $awg_ver"
|
||
else
|
||
log_warn "Команда awg не найдена!"
|
||
fi
|
||
|
||
log "Шаг 3 завершен."
|
||
update_state 4
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 4: Настройка фаервола
|
||
# ==============================================================================
|
||
|
||
step4_setup_firewall() {
|
||
update_state 4
|
||
if [[ "$NO_TWEAKS" -eq 0 ]]; then
|
||
log "### ШАГ 4: Настройка фаервола UFW ###"
|
||
install_packages ufw
|
||
setup_improved_firewall || die "Ошибка настройки UFW."
|
||
log "Шаг 4 завершен."
|
||
else
|
||
log "### ШАГ 4: Пропуск настройки UFW (--no-tweaks) ###"
|
||
fi
|
||
update_state 5
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 5: Скачивание скриптов (БЕЗ Python!)
|
||
# ==============================================================================
|
||
|
||
verify_sha256() {
|
||
local file="$1" expected="$2" label="$3"
|
||
# Пропускаем проверку если:
|
||
# - SHA не установлен (RELEASE_PLACEHOLDER — ещё не выпущен release)
|
||
# - AWG_BRANCH переопределён (тестовая ветка)
|
||
if [[ "$expected" == "RELEASE_PLACEHOLDER" ]]; then
|
||
log_debug "SHA256 для $label: пропуск (placeholder, до release)."
|
||
return 0
|
||
fi
|
||
if [[ "${AWG_BRANCH}" != "v${SCRIPT_VERSION}" ]]; then
|
||
log_warn "SHA256 для $label: проверка пропущена (AWG_BRANCH=${AWG_BRANCH} != v${SCRIPT_VERSION}). Файл не верифицирован."
|
||
return 0
|
||
fi
|
||
local actual
|
||
actual=$(sha256sum "$file" 2>/dev/null | awk '{print $1}')
|
||
if [[ "$actual" != "$expected" ]]; then
|
||
log_error "SHA256 $label НЕ совпадает!"
|
||
log_error " Ожидался: $expected"
|
||
log_error " Получен: $actual"
|
||
log_error " Файл мог быть подменён. Скачайте installer заново с GitHub."
|
||
return 1
|
||
fi
|
||
log_debug "SHA256 $label: OK ($actual)"
|
||
return 0
|
||
}
|
||
|
||
# _secure_download <url> <target> <expected_sha256> <label>
|
||
# Atomic download:
|
||
# 1. curl → mktemp на том же FS, что и target;
|
||
# 2. verify_sha256 на temp (не на target, чтобы corrupt-файл не оказался
|
||
# на целевом пути даже на долю секунды);
|
||
# 3. chmod 700 на temp;
|
||
# 4. mv -f temp → target (атомарный rename).
|
||
# Если любой шаг падает — temp удаляется, target не трогается.
|
||
_secure_download() {
|
||
local url="$1" target="$2" expected_sha256="$3" label="$4"
|
||
local tmp target_dir
|
||
target_dir=$(dirname "$target")
|
||
tmp=$(mktemp -p "$target_dir" ".${label//\//_}.tmp.XXXXXX") \
|
||
|| die "Не удалось создать временный файл для $label"
|
||
if ! curl -fLso "$tmp" --max-time 60 --retry 2 "$url"; then
|
||
rm -f "$tmp" 2>/dev/null
|
||
die "Ошибка скачивания $label"
|
||
fi
|
||
if ! verify_sha256 "$tmp" "$expected_sha256" "$label"; then
|
||
rm -f "$tmp" 2>/dev/null
|
||
die "Целостность $label не подтверждена (SHA256 mismatch). Установка прервана."
|
||
fi
|
||
if ! chmod 700 "$tmp"; then
|
||
rm -f "$tmp" 2>/dev/null
|
||
die "Ошибка chmod $label"
|
||
fi
|
||
if ! mv -f "$tmp" "$target"; then
|
||
rm -f "$tmp" 2>/dev/null
|
||
die "Ошибка перемещения $label на целевой путь"
|
||
fi
|
||
log "$label скачан и верифицирован."
|
||
}
|
||
|
||
step5_download_scripts() {
|
||
update_state 5
|
||
log "### ШАГ 5: Скачивание скриптов управления ###"
|
||
cd "$AWG_DIR" || die "Ошибка перехода в $AWG_DIR"
|
||
|
||
log "Скачивание $COMMON_SCRIPT_PATH..."
|
||
_secure_download "$COMMON_SCRIPT_URL" "$COMMON_SCRIPT_PATH" \
|
||
"$COMMON_SCRIPT_SHA256" "awg_common.sh"
|
||
|
||
log "Скачивание $MANAGE_SCRIPT_PATH..."
|
||
_secure_download "$MANAGE_SCRIPT_URL" "$MANAGE_SCRIPT_PATH" \
|
||
"$MANAGE_SCRIPT_SHA256" "manage_amneziawg.sh"
|
||
|
||
log "Шаг 5 завершен."
|
||
update_state 6
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 6: Генерация конфигураций (нативная, без awgcfg.py)
|
||
# ==============================================================================
|
||
|
||
step6_generate_configs() {
|
||
update_state 6
|
||
log "### ШАГ 6: Генерация конфигураций AWG 2.0 ###"
|
||
cd "$AWG_DIR" || die "Ошибка cd $AWG_DIR"
|
||
|
||
# Подключаем общую библиотеку
|
||
if [[ ! -f "$COMMON_SCRIPT_PATH" ]]; then
|
||
die "awg_common.sh не найден. Шаг 5 не выполнен?"
|
||
fi
|
||
# shellcheck source=/dev/null
|
||
source "$COMMON_SCRIPT_PATH"
|
||
|
||
# Создаём директорию для ключей
|
||
mkdir -p "$KEYS_DIR" || die "Ошибка создания $KEYS_DIR"
|
||
|
||
# Генерация серверных ключей (если ещё нет)
|
||
if [[ ! -f "$AWG_DIR/server_private.key" ]]; then
|
||
log "Генерация серверных ключей..."
|
||
generate_server_keys || die "Ошибка генерации серверных ключей."
|
||
else
|
||
log "Серверные ключи уже существуют."
|
||
fi
|
||
|
||
# Бэкап существующего серверного конфига ДО перезаписи
|
||
if [[ -f "$SERVER_CONF_FILE" ]]; then
|
||
local s_bak
|
||
s_bak="${SERVER_CONF_FILE}.bak-$(date +%F_%H%M%S)"
|
||
cp "$SERVER_CONF_FILE" "$s_bak" || log_warn "Ошибка бэкапа $s_bak"
|
||
log "Бэкап серверного конфига: $s_bak"
|
||
fi
|
||
|
||
# Создание серверного конфига AWG 2.0 с переносом ВСЕХ существующих
|
||
# [Peer]-блоков из бэкапа ОДНОЙ атомарной записью (render_server_config
|
||
# доклеивает пиры в temp ДО mv). Раньше append шёл ПОСЛЕ render отдельной
|
||
# операцией: сбой в окне между ними оставлял живой конфиг без пиров, а
|
||
# повторный запуск шага 6 бэкапил уже безпировый файл - все клиенты
|
||
# терялись при --force reinstall (восстановление только вручную из
|
||
# timestamped .bak).
|
||
# C5-история (важно сохранить семантику): восстанавливаются ВСЕ блоки,
|
||
# включая дефолтные my_phone/my_laptop - идемпотентный цикл ниже
|
||
# пропускает уже существующие пиры, а guard в generate_client отвергает
|
||
# повторное создание при наличии артефактов.
|
||
log "Создание серверного конфига..."
|
||
render_server_config "${s_bak:-}" || die "Ошибка создания серверного конфига."
|
||
if [[ -n "${s_bak:-}" && -f "$s_bak" ]] && grep -q '^\[Peer\]' "$s_bak" 2>/dev/null; then
|
||
log "Существующие пиры восстановлены из бэкапа."
|
||
fi
|
||
|
||
# Генерация клиентов по умолчанию
|
||
log "Создание клиентов по умолчанию..."
|
||
local client_name
|
||
for client_name in my_phone my_laptop; do
|
||
if grep -qxF "#_Name = ${client_name}" "$SERVER_CONF_FILE" 2>/dev/null; then
|
||
log "Клиент '$client_name' уже существует."
|
||
else
|
||
log "Создание клиента '$client_name'..."
|
||
generate_client "$client_name" || log_warn "Ошибка создания клиента '$client_name'"
|
||
fi
|
||
done
|
||
|
||
# Валидация конфига
|
||
validate_awg_config || log_warn "Валидация конфига выявила проблемы."
|
||
|
||
# Установка прав доступа
|
||
secure_files
|
||
|
||
log "Конфигурационные файлы в $AWG_DIR:"
|
||
ls -la "$AWG_DIR"/*.conf "$AWG_DIR"/*.png 2>/dev/null | while IFS= read -r line; do
|
||
log " $line"
|
||
done
|
||
|
||
log "Шаг 6 завершен."
|
||
update_state 7
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 7: Запуск сервиса
|
||
# ==============================================================================
|
||
|
||
step7_start_service() {
|
||
update_state 7
|
||
log "### ШАГ 7: Запуск сервиса и настройка безопасности ###"
|
||
|
||
log "Включение и запуск awg-quick@awg0..."
|
||
|
||
# Переключение изоляции on->off: PostDown нового конфига DROP-правило не
|
||
# снимет (его там больше нет), а down-фаза restart работает уже с новым
|
||
# конфигом на диске. Убираем stale-правила явно, циклом - при повторных
|
||
# прерванных запусках их могло накопиться несколько (issue #178, паттерн
|
||
# отложенной уборки как у PREV_AWG_PORT в #175).
|
||
if [[ "${CLIENT_ISOLATION:-1}" -eq 0 ]]; then
|
||
while iptables -D FORWARD -i awg0 -o awg0 -j DROP 2>/dev/null; do :; done
|
||
while ip6tables -D FORWARD -i awg0 -o awg0 -j DROP 2>/dev/null; do :; done
|
||
fi
|
||
|
||
if systemctl is-active --quiet awg-quick@awg0; then
|
||
log "Сервис уже активен — перезапуск для применения конфигурации..."
|
||
systemctl enable awg-quick@awg0 || log_warn "Не удалось enable awg-quick@awg0 — проверьте автозапуск вручную"
|
||
systemctl restart awg-quick@awg0 || die "Ошибка restart awg-quick@awg0."
|
||
else
|
||
systemctl enable --now awg-quick@awg0 || die "Ошибка enable --now."
|
||
fi
|
||
# Интерфейс только что поднят - фиксируем набор device-параметров, чтобы
|
||
# управляющий скрипт с самого начала знал, что стоит на живом интерфейсе.
|
||
# Без этого первое же обнаружение снятия сравнивать было бы не с чем.
|
||
awg_record_device_params
|
||
log "Сервис включен и запущен."
|
||
|
||
log "Проверка статуса сервиса..."
|
||
local _attempt
|
||
for _attempt in 1 2 3 4 5; do
|
||
sleep 1
|
||
check_service_status 2>/dev/null && break
|
||
[[ $_attempt -lt 5 ]] && log_debug "Ожидание запуска сервиса... (попытка $_attempt/5)"
|
||
done
|
||
check_service_status || die "Проверка статуса сервиса не пройдена."
|
||
|
||
# Fail2Ban
|
||
if [[ "$NO_TWEAKS" -eq 0 ]]; then
|
||
setup_fail2ban
|
||
else
|
||
log "Пропуск Fail2Ban (--no-tweaks)."
|
||
fi
|
||
|
||
log "Шаг 7 успешно завершен."
|
||
update_state 99
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ШАГ 99: Завершение
|
||
# ==============================================================================
|
||
|
||
step99_finish() {
|
||
log "### ЗАВЕРШЕНИЕ УСТАНОВКИ ###"
|
||
log "=============================================================================="
|
||
log "Установка и настройка AmneziaWG 2.0 УСПЕШНО ЗАВЕРШЕНА!"
|
||
log " "
|
||
log "КЛИЕНТСКИЕ ФАЙЛЫ:"
|
||
log " Конфиги (.conf) и QR-коды (.png) в: $AWG_DIR"
|
||
log " Скопируйте их безопасным способом."
|
||
log " Пример (на вашем ПК):"
|
||
log " scp root@<IP_СЕРВЕРА>:$AWG_DIR/*.conf ./"
|
||
log " "
|
||
log "ПОЛЕЗНЫЕ КОМАНДЫ:"
|
||
log " sudo bash $MANAGE_SCRIPT_PATH help # Управление клиентами"
|
||
log " systemctl status awg-quick@awg0 # Статус VPN"
|
||
log " awg show # Статус AmneziaWG"
|
||
log " ufw status verbose # Статус Firewall"
|
||
log " "
|
||
log "ВАЖНО: Для подключения используйте клиент Amnezia VPN >= 4.8.12.7"
|
||
log " с поддержкой протокола AWG 2.0"
|
||
log " "
|
||
cleanup_apt
|
||
log " "
|
||
|
||
# Финальные проверки
|
||
if [[ -f "$CONFIG_FILE" ]]; then
|
||
log "Файл настроек $CONFIG_FILE: OK"
|
||
else
|
||
log_error "Файл настроек $CONFIG_FILE ОТСУТСТВУЕТ!"
|
||
fi
|
||
|
||
# Удаление файла состояния
|
||
log "Удаление файла состояния установки..."
|
||
rm -f "$STATE_FILE" "${STATE_FILE}.lock" "$AWG_DIR/.boot_id_before_step2" || log_warn "Не удалось удалить $STATE_FILE"
|
||
log "Установка полностью завершена. Лог: $LOG_FILE"
|
||
log "=============================================================================="
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Основной цикл выполнения
|
||
# ==============================================================================
|
||
|
||
if [[ "$HELP" -eq 1 ]]; then show_help; fi
|
||
if [[ "$UNINSTALL" -eq 1 ]]; then step_uninstall; fi
|
||
if [[ "$DIAGNOSTIC" -eq 1 ]]; then create_diagnostic_report; exit 0; fi
|
||
if [[ "$VERBOSE" -eq 1 ]]; then set -x; fi
|
||
|
||
# v5.13.0: idempotency-страж — если AmneziaWG уже установлен и работает,
|
||
# повторный запуск даром тратит ~20 минут (Step 1 ещё раз настраивает sysctl/swap/BBR,
|
||
# `apt-get upgrade` может подтянуть новое ядро и заставить пользователя
|
||
# заново перезагружаться, Step 7 рестартит awg-quick@awg0 — handshake
|
||
# отваливаются на несколько секунд). Серверные ключи, пиры и параметры
|
||
# обфускации сохраняются при повторе, но без явного opt-in это поведение
|
||
# выглядит как «тихая переустановка». Защищаемся явным флагом.
|
||
# Поднимает ENV AWG_FORCE_REINSTALL=1 ровно так же, как CLI-флаг.
|
||
if [[ "${AWG_FORCE_REINSTALL:-0}" == "1" ]]; then
|
||
FORCE_REINSTALL=1
|
||
fi
|
||
if [[ "$FORCE_REINSTALL" -ne 1 ]] && [[ -f "$SERVER_CONF_FILE" ]] \
|
||
&& systemctl is-active --quiet awg-quick@awg0 2>/dev/null; then
|
||
log_error "AmneziaWG уже установлен и запущен."
|
||
log_error "Чтобы переустановить — добавьте --force (или AWG_FORCE_REINSTALL=1)."
|
||
log_error "ВНИМАНИЕ: переустановка снова прогонит шаги 1 (sysctl/swap/BBR) и 7 (рестарт сервиса)."
|
||
log_error " Параметры обфускации (Jc/Jmin/Jmax/H1-H4/I1) сохранятся, ЕСЛИ не передавать"
|
||
log_error " --preset/--jc/--jmin/--jmax (эти флаги перегенерируют весь набор - все"
|
||
log_error " выданные клиентские конфиги придётся перевыпустить через regen)."
|
||
log_error "Для управления клиентами: sudo bash $MANAGE_SCRIPT_PATH help"
|
||
log_error "Для полного удаления: sudo bash $0 --uninstall"
|
||
exit 0
|
||
fi
|
||
|
||
initialize_setup
|
||
|
||
while (( current_step < 99 )); do
|
||
log "Выполнение шага $current_step..."
|
||
case $current_step in
|
||
1) step1_update_and_optimize ;;
|
||
2) step2_install_amnezia ;;
|
||
3) step3_check_module; current_step=4 ;;
|
||
4) step4_setup_firewall; current_step=5 ;;
|
||
5) step5_download_scripts; current_step=6 ;;
|
||
6) step6_generate_configs; current_step=7 ;;
|
||
7) step7_start_service; current_step=99 ;;
|
||
*) die "Ошибка: Неизвестный шаг $current_step." ;;
|
||
esac
|
||
done
|
||
|
||
if (( current_step == 99 )); then step99_finish; fi
|
||
exit 0
|