#!/usr/bin/env bash # install.sh — разворачивает AmneziaWG и команду vpn на свежем сервере одной командой: # wget -qO- https://a.dev.onov.ru | sudo bash # Ставит официальный amneziawg-installer с --yes. Сам установщик всегда работает внутри # одноразового systemd-юнита (режим --resume): обрыв SSH его не прерывает, а после # каждого ребута юнит запускает его снова, пока установщик не отметит завершение. # В терминале запускатель только готовит файлы, стартует юнит и показывает лог. # Доп. флаги установщика: sudo AWG_INSTALL_ARGS="--port=443" bash install.sh set -euo pipefail # Неизменяемые константы. scripts/build.sh встраивает этот файл в dist/install.sh и dist/vpn # вместо строки-маркера, поэтому собранные скрипты самодостаточны. # BOOTSTRAP_ROOT — префикс всех абсолютных путей, нужен только тестам (в проде пуст). # shellcheck disable=SC2034 readonly BASE_URL="https://a.dev.onov.ru" readonly AWG_INSTALLER_URL="https://raw.githubusercontent.com/bivlked/amneziawg-installer/main/install_amneziawg.sh" readonly FS_ROOT="${BOOTSTRAP_ROOT:-}" readonly AWG_DIR="$FS_ROOT/root/awg" readonly AWG_STATE_FILE="$AWG_DIR/setup_state" readonly AWG_STATE_DONE="99" readonly AWG_MANAGE="$AWG_DIR/manage_amneziawg.sh" readonly AWG_SERVER_CONF="$FS_ROOT/etc/amnezia/amneziawg/awg0.conf" readonly BOOTSTRAP_DIR="$FS_ROOT/opt/awg-bootstrap" readonly BOOTSTRAP_UNIT="awg-bootstrap.service" readonly BOOTSTRAP_UNIT_FILE="$FS_ROOT/etc/systemd/system/$BOOTSTRAP_UNIT" readonly BOOTSTRAP_LOG="$FS_ROOT/var/log/awg-bootstrap.log" readonly BOOTSTRAP_MOTD="$FS_ROOT/etc/motd.d/awg-bootstrap" readonly BOOTSTRAP_MAX_ATTEMPTS=6 readonly VPN_BIN="$FS_ROOT/usr/local/bin/vpn" RESUME=0 [[ ${1:-} == "--resume" ]] && RESUME=1 readonly STAGED_INSTALLER="$BOOTSTRAP_DIR/install_amneziawg.sh" readonly STAGED_VPN="$BOOTSTRAP_DIR/vpn" # Под юнитом stdout уже дописывается в лог (StandardOutput=append:), поэтому # в файл явно пишет только запускатель. log() { local line line="[$(date +'%F %T')] $*" echo "$line" if [[ $RESUME -eq 0 && -w ${BOOTSTRAP_LOG%/*} ]]; then echo "$line" >>"$BOOTSTRAP_LOG"; fi return 0 } die() { echo "ОШИБКА: $*" >&2 if [[ $RESUME -eq 0 && -w ${BOOTSTRAP_LOG%/*} ]]; then echo "[$(date +'%F %T')] ОШИБКА: $*" >>"$BOOTSTRAP_LOG" fi exit 1 } # fetch — скачать файл через curl, а если его нет, через wget. # Код 127 (команда не найдена/не запускается) переключает на следующий инструмент. # Пишет во временный файл и подменяет dest атомарно: читающий его процесс не увидит половину. fetch() { local url="$1" dest="$2" rc=127 if command -v curl >/dev/null; then curl -fsSL --retry 5 --retry-all-errors -o "$dest.part" "$url" && rc=0 || rc=$? fi if [[ $rc -eq 127 ]] && command -v wget >/dev/null; then wget -qO "$dest.part" "$url" && rc=0 || rc=$? fi [[ $rc -eq 127 ]] && die "нет ни curl, ни wget: установите один из них (apt install -y curl)" if [[ $rc -ne 0 ]]; then rm -f "$dest.part" return "$rc" fi mv -f "$dest.part" "$dest" } require_root() { [[ ${BOOTSTRAP_EUID:-$EUID} -eq 0 ]] || die "нужны права root: запустите через sudo" } # install_self — положить копию скрипта в BOOTSTRAP_DIR и продолжить работу оттуда: # её запускает systemd-юнит. При запуске через pipe ($0 = bash) файла скрипта нет, # тогда копия скачивается с BASE_URL. install_self() { local self="$BOOTSTRAP_DIR/install.sh" [[ $0 == "$self" ]] && return 0 mkdir -p "$BOOTSTRAP_DIR" "${BOOTSTRAP_LOG%/*}" chmod 700 "$BOOTSTRAP_DIR" if [[ -f $0 && $0 == *install.sh ]]; then cp "$0" "$self.part" mv -f "$self.part" "$self" else fetch "$BASE_URL/install.sh" "$self" || die "не удалось скачать $BASE_URL/install.sh" fi chmod 700 "$self" # stdin при запуске через pipe — это текст скрипта: дочерние процессы не должны его читать. exec bash "$self" "$@" /dev/null || true) [[ $st == active || $st == activating ]] } enable_resume_unit() { mkdir -p "${BOOTSTRAP_UNIT_FILE%/*}" cat >"$BOOTSTRAP_UNIT_FILE" <"$f" echo "$n" } attempts() { local f="$BOOTSTRAP_DIR/attempts" if [[ -f $f ]]; then cat "$f"; else echo 0; fi } shutdown_pending() { [[ $(systemctl is-system-running 2>/dev/null || true) == "stopping" ]] && return 0 systemctl list-jobs 2>/dev/null | grep -qE 'reboot\.target|poweroff\.target' } # run_installer — запустить сохранённую копию установщика с --yes и сохранёнными флагами. # Копия скачивается один раз: все шаги установки выполняет одна и та же версия. run_installer() { local extra="" rc=0 local -a args if [[ ! -f $STAGED_INSTALLER ]]; then fetch "$AWG_INSTALLER_URL" "$STAGED_INSTALLER" || die "не удалось скачать установщик: $AWG_INSTALLER_URL" fi [[ -f $BOOTSTRAP_DIR/args ]] && extra=$(<"$BOOTSTRAP_DIR/args") read -ra args <<<"$extra" log "Запуск установщика AmneziaWG (попытка $(bump_attempts) из $BOOTSTRAP_MAX_ATTEMPTS): --yes ${args[*]}" bash "$STAGED_INSTALLER" --yes "${args[@]}" "$BOOTSTRAP_MOTD" } finish_ok() { install_vpn disable_resume_unit rm -f "$BOOTSTRAP_DIR/attempts" write_motd ok log "Готово. AmneziaWG установлен, управление клиентами: vpn help" } finish_fail() { write_motd fail die "$1" } # worker — режим --resume, работает внутри юнита. Один экземпляр за раз (flock). worker() { exec 9>"$BOOTSTRAP_DIR/lock" flock -n 9 || { log "Установка уже выполняется другим процессом — выхожу." return 0 } if is_installed; then finish_ok return 0 fi if [[ $(attempts) -ge $BOOTSTRAP_MAX_ATTEMPTS ]]; then disable_resume_unit finish_fail "установщик не завершился за $BOOTSTRAP_MAX_ATTEMPTS попыток, автоповтор выключен. Лог: $BOOTSTRAP_LOG" fi write_motd progress local rc=0 run_installer || rc=$? if is_installed; then finish_ok return 0 fi if shutdown_pending; then log "Сервер уходит в перезагрузку, после неё установка продолжится сама." return 0 fi # Юнит остаётся включённым: установка повторится при следующей загрузке. finish_fail "установщик завершился с кодом $rc (шаг $(awg_state)). Лог: $BOOTSTRAP_LOG. Повторить: sudo $BOOTSTRAP_DIR/install.sh" } # follow_unit — показывать лог с позиции offset, пока юнит работает. follow_unit() { local offset="$1" tail_pid tail -c +"$((offset + 1))" -F "$BOOTSTRAP_LOG" 2>/dev/null & tail_pid=$! while unit_running; do sleep 2; done sleep 1 kill "$tail_pid" 2>/dev/null || true wait "$tail_pid" 2>/dev/null || true } log_size() { if [[ -f $BOOTSTRAP_LOG ]]; then wc -c <"$BOOTSTRAP_LOG"; else echo 0; fi } report() { if is_installed; then log "Готово. AmneziaWG установлен, управление клиентами: vpn help" elif shutdown_pending; then log "Сервер уходит в перезагрузку, установка продолжится сама. После входа по SSH ход установки: tail -f $BOOTSTRAP_LOG" else die "установка не завершилась. Лог: $BOOTSTRAP_LOG. Повторить: sudo $BOOTSTRAP_DIR/install.sh" fi } # launcher — ручной запуск: подготовить файлы, стартовать юнит и показать его лог. launcher() { if unit_running; then log "Установка уже идёт — показываю лог (Ctrl+C не прерывает установку)." follow_unit "$(log_size)" report return 0 fi if is_installed; then log "AmneziaWG уже установлен — обновляю команду vpn." fetch "$BASE_URL/vpn" "$STAGED_VPN" || die "не удалось скачать $BASE_URL/vpn" finish_ok return 0 fi # Всё, что нужно из сети, скачивается сейчас: после ребута сеть может подняться не сразу. fetch "$BASE_URL/vpn" "$STAGED_VPN" || die "не удалось скачать $BASE_URL/vpn" if [[ ! -f $STAGED_INSTALLER ]]; then fetch "$AWG_INSTALLER_URL" "$STAGED_INSTALLER" || die "не удалось скачать установщик: $AWG_INSTALLER_URL" fi printf '%s' "${AWG_INSTALL_ARGS:-}" >"$BOOTSTRAP_DIR/args" # Лимит попыток защищает от бесконечных ребутов по кругу, а не от ручного повтора. rm -f "$BOOTSTRAP_DIR/attempts" : >>"$BOOTSTRAP_LOG" chmod 640 "$BOOTSTRAP_LOG" enable_resume_unit log "Установка запущена в фоне (юнит $BOOTSTRAP_UNIT) — обрыв SSH её не прервёт, Ctrl+C тоже." local offset offset=$(log_size) systemctl start --no-block "$BOOTSTRAP_UNIT" follow_unit "$offset" report } main() { require_root install_self "$@" if [[ $RESUME -eq 1 ]]; then worker; else launcher; fi } main "$@"