# Copyright (c) 2026 Ivan Vasilev # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. #!/usr/bin/env bash # sudo bash <(curl -Ls https://amnesia.ma7neko.ru/forgeting/install.sh) set -euo pipefail GRN='\033[0;32m'; YLW='\033[1;33m'; RED='\033[0;31m'; BLU='\033[0;34m' BLD='\033[1m'; NC='\033[0m' ok() { echo -e " ${GRN}✓${NC} $*"; } warn() { echo -e " ${YLW}⚠${NC} $*"; } fail() { trap - ERR; echo -e "\n ${RED}✗${NC} $*" >&2; exit 1; } step() { echo -e "\n${BLD}${BLU}── $* ──${NC}"; } trap 'rc=$?; echo -e "\n ${RED}✗ НЕОЖИДАННАЯ ОШИБКА${NC} строка ${LINENO} код ${rc}\n команда: ${BASH_COMMAND}\n полный лог: ${LOGFILE:-<ещё не открыт>}" >&2' ERR [[ $EUID -ne 0 ]] && fail "Запусти от root: sudo bash install.sh" [[ -z "${BASH_VERSION:-}" ]] && fail "Нужен bash: bash install.sh" VERSION="0.1.3.1" BASE_URL="https://amnesia.ma7neko.ru/forgeting" # хост с install.sh и архивами PROJECT="/opt/awg-control" AMNEZIA_DIR="/etc/amnezia" AWG_DIR="$AMNEZIA_DIR/amneziawg" PRIV_KEY_FILE="$AMNEZIA_DIR/server_private.key" PUB_KEY_FILE="$AWG_DIR/server_public.key" AWG_CONF="$AWG_DIR/awg1.conf" DB_FILE="$AWG_DIR/users.db" UI_DB_FILE="$AWG_DIR/ui.db" # своя БД awg-ui (API-ключи); вне PROJECT — переживает переустановку # Внутренняя авторизация awg-ui → awg-ctrl (Ed25519): приватный → awg-ui, публичный → awg-ctrl. INTERNAL_AUTH_PRIV="$AWG_DIR/internal_auth_private.key" INTERNAL_AUTH_PUB="$AWG_DIR/internal_auth_public.key" IFACE="awg1" AWG_PORT="47619" SUBNET="10.9" MTU="1376" # Запуск CLI: tsx напрямую из node_modules (не npx) — чтобы PID был реальным # (та же причина, что у обёртки /usr/local/bin/awg-ctrl). Нужно обоим режимам. TSX="$PROJECT/cli/node_modules/.bin/tsx" CLI="$PROJECT/cli/src/index.ts" echo -e "${BLD}Forgetting Alpha ${VERSION}${NC}" echo # Запускается ДО вопросов и любого деструктива (rm -rf): на несовместимой # машине лучше упасть сразу, а не после ввода данных или удаления каталога. # Порты (UDP VPN / порт UI) здесь НЕ проверяем — это отдельно (фаервол/облако). echo -e "${BLD}Проверка совместимости:${NC}" KERNEL=$(uname -r) VIRT=$(systemd-detect-virt 2>/dev/null || echo "unknown") echo " Ядро: $KERNEL · виртуализация: $VIRT · $(lsb_release -ds 2>/dev/null || echo unknown)" # Считаем все три пункта, НЕ падая на первом, — чтобы показать полный чеклист # со статусом по каждому. Если хоть один не прошёл — печатаем причины и выходим # с кодом 0 (чистый выход, без вида «упало с ошибкой»). VIRT_OK=1; HDR_OK=1; NET_OK=1 VIRT_WHY=""; HDR_WHY=""; NET_WHY="" # 1) Виртуализация: kernel-модуль нельзя загрузить там, где ядро общее с хостом. case "$VIRT" in openvz|lxc|lxc-libvirt|docker|podman|wsl) VIRT_OK=0 VIRT_WHY="виртуализация '$VIRT' — ядро общее с хостом, kernel-модуль AmneziaWG не загрузить (нужен userspace amneziawg-go)" ;; esac # 2) Заголовки ядра: DKMS соберёт модуль только при их наличии под текущее ядро. # Уже в системе или есть кандидат в apt — ок. «(none)» — кастомное ядро, не # проходит. Пустой apt-индекс не валим: добьёт поздняя проверка на шаге AWG. HDR_POLICY=$(apt-cache policy "linux-headers-$KERNEL" 2>/dev/null || true) if [[ -d "/lib/modules/$KERNEL/build" ]]; then : elif echo "$HDR_POLICY" | grep -q 'Candidate: [^(]'; then : elif echo "$HDR_POLICY" | grep -q 'Candidate: (none)'; then HDR_OK=0 HDR_WHY="нет заголовков под ядро $KERNEL в apt (кастомное ядро провайдера). Решение: apt-get install -y linux-generic && reboot, затем запусти install.sh заново в generic-ядре" fi # 3) Доступ в интернет: нужен для архива, Node и пакетов AWG. Без -f: любой # HTTP-ответ = связь есть; ненулевой код только при сбое соединения/DNS. if ! curl -sS --connect-timeout 8 -o /dev/null "$BASE_URL/" 2>/dev/null; then NET_OK=0 NET_WHY="нет доступа к $BASE_URL — проверь интернет и DNS" fi # Чеклист: ✓ — пройдено, ✗ — нет. mark() { [[ "$1" == 1 ]] && echo -e " ${GRN}✓${NC} $2" || echo -e " ${RED}✗${NC} $2"; } mark "$VIRT_OK" "Виртуализация" mark "$HDR_OK" "Заголовки ядра" mark "$NET_OK" "Доступ в интернет" if [[ "$VIRT_OK" == 0 || "$HDR_OK" == 0 || "$NET_OK" == 0 ]]; then echo warn "Установка невозможна — не пройдены проверки:" [[ "$VIRT_OK" == 0 ]] && echo -e " ${RED}•${NC} $VIRT_WHY" [[ "$HDR_OK" == 0 ]] && echo -e " ${RED}•${NC} $HDR_WHY" [[ "$NET_OK" == 0 ]] && echo -e " ${RED}•${NC} $NET_WHY" echo trap - ERR exit 0 fi # Наличие базы определяем заранее и молча — от него зависит, спросим ли ниже имя # сервера (если база есть, имя берётся из неё). Без вывода. KEEP_DATA — отдельно. DB_EXISTS="n" [[ -f "$DB_FILE" ]] && DB_EXISTS="y" echo # TODO: рандомизировать параметры обфускации при установке. # awg-ctrl уже читает их из [Interface] awg1.conf (readAwgParams), поэтому # достаточно генерировать случайные значения здесь — править awg-ctrl не нужно. # Ограничения, которые обязан соблюсти генератор: # - Jc: 3–10 (больше — лишний трафик); Jmin < Jmax, оба < MTU # - S1, S2: < ~150, в части версий S1 != S2 # - H1–H4: уникальны между собой, НЕ равны 1/2/3/4 (зарезервированные # типы сообщений WireGuard), большие uint32 без пересечений # - I1–I5 НЕ трогать: сейчас уходят в vpn:// ключ пустыми плейсхолдерами # Параметры фиксируются на весь срок жизни сервера: при KEEP_DATA=y их # менять нельзя (иначе все ранее выданные vpn:// ключи станут невалидными). JC=6; JMIN=10; JMAX=50 S1=90; S2=45; S3=37; S4=14 H1="1224800044-2116730834" H2="2122053282-2133204808" H3="2133604274-2140756116" H4="2143656228-2147444225" NET_IFACE=$(ip route show default 2>/dev/null | awk '/default/{print $5; exit}') [[ -z "$NET_IFACE" ]] && fail "Не могу определить сетевой интерфейс" # Сначала корректно через CLI (по PID-файлам), затем добиваем всё, что ещё # держит файлы проекта: осиротевшие процессы, ручной запуск или stale PID, # которые `stop all` не находит. Вызывать ДО `rm -rf $PROJECT` — иначе node # продолжит работать с уже удалёнными файлами и порт/интерфейс останутся занятыми. kill_related() { local _tsx="$PROJECT/cli/node_modules/.bin/tsx" local _cli="$PROJECT/cli/src/index.ts" [[ -x "$_tsx" && -f "$_cli" ]] && "$_tsx" "$_cli" stop all 2>/dev/null || true # cmdline всех сервисов (tsx awg-ctrl/awg-ui/cli) содержит путь проекта. if command -v pkill &>/dev/null; then pkill -TERM -f "$PROJECT/" 2>/dev/null || true sleep 1 pkill -KILL -f "$PROJECT/" 2>/dev/null || true else local _pids _pids=$(ps -eo pid=,args= | awk -v p="$PROJECT/" 'index($0,p){print $1}') [[ -n "$_pids" ]] && kill -TERM $_pids 2>/dev/null || true sleep 1 _pids=$(ps -eo pid=,args= | awk -v p="$PROJECT/" 'index($0,p){print $1}') [[ -n "$_pids" ]] && kill -KILL $_pids 2>/dev/null || true fi rm -f /tmp/awg-ctrl.pid /tmp/awg-ui.pid 2>/dev/null || true } # Логирование: дублируем stdout/stderr в файл лога через tee. LOGFILE — глобал, # на него ссылается ERR-трап. start_logging() { LOGFILE="/var/log/awg-install-$(date +%Y%m%d-%H%M%S).log" exec > >(tee -a "$LOGFILE") 2>&1 echo -e " ${BLD}Лог установки:${NC} $LOGFILE" } # Скачать архив версии VERSION, распаковать в PROJECT и проверить, что ключевые # файлы на месте. download_extract() { local url="$BASE_URL/awgcontrol-${VERSION}.tar.gz" local tmp="/tmp/awgcontrol-${VERSION}.tar.gz" echo " → версия: ${VERSION}" curl -fsSL --connect-timeout 15 "$url" -o "$tmp" \ || fail "Не удалось скачать архив: $url" ok "Архив скачан: $(du -sh "$tmp" | cut -f1)" mkdir -p "$PROJECT" tar -xzf "$tmp" -C "$PROJECT" --strip-components=1 \ || fail "Не удалось распаковать архив" rm -f "$tmp" ok "Распакован → $PROJECT" local f for f in \ "$PROJECT/awg-ctrl/index.ts" \ "$PROJECT/awg-ctrl/package.json" \ "$PROJECT/awg-ui/public/index.html" \ "$PROJECT/awg-ui/server.ts" \ "$PROJECT/awg-ui/package.json" \ "$PROJECT/cli/src/index.ts" \ "$PROJECT/cli/package.json" do [[ -f "$f" ]] || fail "Файл не найден после распаковки: $f" done ok "Архив проверен" } # npm install во всех трёх сервисах. npm_install_all() { local SVC for SVC in awg-ctrl awg-ui cli; do if [[ -f "$PROJECT/$SVC/package.json" ]]; then echo -n " $SVC ... " (cd "$PROJECT/$SVC" && npm install --silent) \ || fail "npm install в $SVC завершился ошибкой" echo -e "${GRN}ok${NC}" else warn "$PROJECT/$SVC/package.json не найден — пропуск" fi done } # systemd-юнит: автозапуск awg-control на загрузке. Модель CLI — детач-процессы # с PID-файлами (а не один долгоживущий процесс), поэтому Type=oneshot + # RemainAfterExit: systemd держит юнит «active», процессами рулит CLI через # start/stop. awg-ctrl падает, если awg1 не поднят, а на загрузке awg-quick up # вручную не выполняется — поднимаем интерфейс в ExecStartPre (если ещё не поднят). SERVICE_UNIT="/etc/systemd/system/awg-control.service" setup_service() { # node может стоять вне дефолтного PATH systemd (nvm и т.п.), а tsx # запускается через shebang `#!/usr/bin/env node` — иначе ExecStart падает с # кодом 127 «node not found». Прописываем реальный каталог node в PATH юнита; # sbin тоже включаем (awg-quick зовёт iptables/ip/sysctl). local node_dir node_dir=$(dirname "$(command -v node 2>/dev/null || echo /usr/bin/node)") cat > "$SERVICE_UNIT" </dev/null 2>&1 || awg-quick up ${IFACE}' ExecStart=/usr/local/bin/awg-ctrl start all ExecStop=/usr/local/bin/awg-ctrl stop all [Install] WantedBy=multi-user.target UNIT systemctl daemon-reload systemctl enable awg-control.service >/dev/null 2>&1 \ || warn "systemctl enable awg-control не удался — автозапуск не настроен" ok "systemd-юнит awg-control установлен (автозапуск на загрузке)" } # Запуск/перезапуск сервисов + статус. Через systemd, если он есть (тогда # работает автозапуск на загрузке); иначе — напрямую через CLI, без автозапуска. start_and_status() { if command -v systemctl >/dev/null 2>&1; then setup_service systemctl restart awg-control.service \ || fail "systemctl restart awg-control завершился ошибкой" else warn "systemd не найден — запускаю напрямую через CLI (без автозапуска)" "$TSX" "$CLI" start all fi sleep 2 echo "$TSX" "$CLI" status } INSTALL_MODE="fresh" if [[ -d "$PROJECT" ]]; then echo warn "Найден каталог $PROJECT" echo " 1) Обновить до версии ${VERSION} — сохранить настройки и пользователей" echo " 2) Полностью переустановить — удалить каталог и начать заново" read -rp " Выбери [1/2]: " INST_CHOICE case "${INST_CHOICE:-1}" in 1) INSTALL_MODE="update" ;; 2) INSTALL_MODE="fresh" ;; *) fail "Неверный выбор: введи 1 или 2" ;; esac echo fi if [[ "$INSTALL_MODE" == "update" ]]; then start_logging step "Обновление до версии ${VERSION}" echo " → останавливаем и убиваем все процессы awg-control" kill_related download_extract npm_install_all start_and_status echo echo -e "${BLD}${GRN}✓ Обновление завершено — версия ${VERSION}${NC}" echo exit 0 fi # При полной переустановке убиваем все процессы awg-control и очищаем старый # каталог, чтобы не оставалось ни запущенных процессов, ни старых файлов. if [[ -d "$PROJECT" ]]; then kill_related rm -rf "$PROJECT" fi step "Конфигурация" # Server IP определяем автоматически (внешний через ifconfig.me, иначе локальный # по маршруту) — не спрашиваем. Если определить не удалось — падаем. SERVER_IP=$(curl -s4 --connect-timeout 5 ifconfig.me 2>/dev/null \ || ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' \ || echo "") [[ -z "$SERVER_IP" ]] && fail "Не удалось определить Server IP автоматически" ok "Server IP: $SERVER_IP" # Имя сервера спрашиваем ниже — только если базы ещё нет (DB_EXISTS=n). # Если база есть, имя берётся из неё. # Внутренняя авторизация awg-ui ↔ awg-ctrl — асимметричная пара, генерируется # в шаге 4 (Ключи). Здесь секрет не нужен. AWGCTRL_PORT=$(( (RANDOM % 22768) + 32768 )) read -rp " UI port (Enter — случайный): " UI_PORT UI_PORT="${UI_PORT:-$(( (RANDOM % 22768) + 32768 ))}" read -rp " UI логин [admin]: " UI_USER UI_USER="${UI_USER:-admin}" SUGGESTED_PASS=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 12 2>/dev/null || openssl rand -hex 6) read -rsp " UI пароль [$SUGGESTED_PASS]: " UI_PASS; echo UI_PASS="${UI_PASS:-$SUGGESTED_PASS}" JWT_SECRET=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 32 2>/dev/null || openssl rand -hex 16) # Если найдены база пользователей и ключи сервера — предлагаем их сохранить. # Важно: vpn:// ключи пользователей привязаны к ключу сервера, поэтому # сохранять базу имеет смысл только вместе со старым ключом сервера. KEEP_DATA="n" if [[ -f "$DB_FILE" || -f "$PRIV_KEY_FILE" ]]; then echo warn "Найдена существующая установка:" [[ -f "$DB_FILE" ]] && echo " база пользователей: $DB_FILE" [[ -f "$PRIV_KEY_FILE" ]] && echo " ключ сервера: $PRIV_KEY_FILE" if [[ -f "$DB_FILE" && -f "$PRIV_KEY_FILE" && -f "$PUB_KEY_FILE" ]]; then read -rp " Сохранить пользователей и ключ сервера? [Y/n]: " KD [[ "${KD:-y}" =~ ^[Yy]$ || -z "${KD}" ]] && KEEP_DATA="y" else warn "Для сохранения нужны и база, и оба ключа сервера — часть отсутствует." warn "Пользователей не сохранить (vpn:// ключи стали бы невалидными)." fi fi # Имя сервера: если база уже существует (DB_EXISTS, проверено заранее в # preflight) — имя берётся из неё, не спрашиваем. Если базы нет — спрашиваем. if [[ "$DB_EXISTS" == "y" ]]; then SERVER_NAME="VPN" echo " Server name: берётся из существующей базы (пропускаем)" else read -rp " Server name [VPN]: " SERVER_NAME SERVER_NAME="${SERVER_NAME:-VPN}" fi echo " Project dir: $PROJECT" echo " Server IP: $SERVER_IP" echo " Server name: $SERVER_NAME" echo " AWG port: $AWG_PORT (udp)" echo " awgctrl port: $AWGCTRL_PORT" echo " UI port: $UI_PORT" echo " UI логин: $UI_USER" echo " Net interface: $NET_IFACE" if [[ "$KEEP_DATA" == "y" ]]; then echo -e " Данные: ${GRN}сохранить существующих пользователей и ключ${NC}" elif [[ -f "$DB_FILE" ]]; then echo -e " Данные: ${YLW}новая установка (старая база → бэкап)${NC}" fi echo read -rp " Продолжить? [Y/n]: " YN [[ "${YN:-y}" =~ ^[Nn]$ ]] && { echo " Отменено."; exit 0; } start_logging # Совместимость (виртуализация, заголовки ядра, интернет) проверена выше в # секции «Проверка совместимости»; $KERNEL задан там же. step "1/7 AmneziaWG" if command -v awg &>/dev/null && command -v awg-quick &>/dev/null && modinfo amneziawg &>/dev/null; then ok "AWG уже установлен (модуль amneziawg: $(modinfo -F version amneziawg 2>/dev/null || echo present))" else echo " → apt-get update" apt-get update || fail "apt-get update упал — проверь /etc/apt/sources.list*" echo " → установка зависимостей сборки + заголовков ядра" DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a \ apt-get install -y \ software-properties-common \ python3-launchpadlib \ gnupg2 \ dkms \ build-essential \ "linux-headers-$KERNEL" \ linux-headers-generic \ || fail "Не удалось установить зависимости или заголовки ядра" if [[ ! -d "/lib/modules/$KERNEL/build" ]]; then warn "Нет /lib/modules/$KERNEL/build — заголовки под текущее ядро отсутствуют." warn "Часто бывает на кастомном ядре провайдера. Решение:" warn " apt-get install -y linux-generic && reboot" warn "и после загрузки в generic-ядро запусти install.sh заново." fail "Отсутствуют заголовки ядра $KERNEL — DKMS не соберёт модуль" fi ok "Заголовки ядра на месте: /lib/modules/$KERNEL/build" echo " → add-apt-repository ppa:amnezia/ppa" add-apt-repository -y ppa:amnezia/ppa \ || fail "Не удалось добавить PPA ppa:amnezia/ppa" echo " → apt-get update (после PPA)" apt-get update \ || fail "apt-get update после PPA упал — проверь источники amnezia в /etc/apt" echo " → установка amneziawg (сборка DKMS-модуля, может занять до минуты)" if ! DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a \ apt-get install -y amneziawg; then warn "apt-get install amneziawg завершился с ошибкой." MKLOG=$(ls -1t /var/lib/dkms/amneziawg/*/build/make.log 2>/dev/null | head -1 || true) if [[ -n "${MKLOG:-}" && -f "$MKLOG" ]]; then echo " ───── $MKLOG (последние 40 строк) ─────" tail -n 40 "$MKLOG" | sed 's/^/ /' echo " ───────────────────────────────────────────────" else warn "make.log не найден — ошибка, вероятно, на этапе apt/репозиториев." fi fail "Не удалось установить amneziawg (детали выше и в $LOGFILE)" fi echo " → проверка собранного модуля" dkms status amneziawg 2>/dev/null | sed 's/^/ /' || true if ! modprobe amneziawg 2>/dev/null; then MKLOG=$(ls -1t /var/lib/dkms/amneziawg/*/build/make.log 2>/dev/null | head -1 || true) [[ -n "${MKLOG:-}" && -f "$MKLOG" ]] && { echo " ── make.log (tail) ──"; tail -n 40 "$MKLOG" | sed 's/^/ /'; } fail "Модуль amneziawg не загрузился — DKMS-сборка несовместима с ядром" fi ok "AWG установлен, модуль amneziawg собран и загружается" fi step "2/7 Node.js" if command -v node &>/dev/null; then ok "Node.js уже установлен: $(node --version)" else echo " → установка Node.js 20.x (nodesource)" curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs || fail "Не удалось установить Node.js" ok "Node.js установлен: $(node --version)" fi step "3/7 Файлы проекта" download_extract step "4/7 Ключи и конфиг AWG" mkdir -p "$AWG_DIR" "$AMNEZIA_DIR" AWG_BIN=$(which awg || echo /usr/bin/awg) if [[ "$KEEP_DATA" == "y" ]]; then # Переиспользуем существующий ключ сервера — иначе старые vpn:// ключи # пользователей в users.db станут невалидными. PRIV_KEY=$(cat "$PRIV_KEY_FILE") PUB_KEY=$(cat "$PUB_KEY_FILE") ok "Используются существующие ключи сервера (база сохранена): $PUB_KEY" else # Новая установка. Существующую базу не удаляем, а отправляем в бэкап, # чтобы awg-ctrl создал чистую users.db при старте. if [[ -f "$DB_FILE" ]]; then DB_BAK="${DB_FILE}.bak-$(date +%Y%m%d-%H%M%S)" mv "$DB_FILE" "$DB_BAK" warn "Старая база пользователей сохранена: $DB_BAK" fi # ui.db (API-ключи awg-ui) — тоже в бэкап, чтобы awg-ui создал чистую БД. if [[ -f "$UI_DB_FILE" ]]; then UI_DB_BAK="${UI_DB_FILE}.bak-$(date +%Y%m%d-%H%M%S)" mv "$UI_DB_FILE" "$UI_DB_BAK" # WAL-сайдкары удаляем — к новой БД они неприменимы. rm -f "${UI_DB_FILE}-wal" "${UI_DB_FILE}-shm" warn "Старая база API-ключей сохранена: $UI_DB_BAK" fi PRIV_KEY=$(umask 077 && awg genkey) PUB_KEY=$(printf '%s' "$PRIV_KEY" | awg pubkey) umask 077 printf '%s' "$PRIV_KEY" > "$PRIV_KEY_FILE" printf '%s' "$PUB_KEY" > "$PUB_KEY_FILE" chmod 600 "$PRIV_KEY_FILE" "$PUB_KEY_FILE" ok "Публичный ключ: $PUB_KEY" fi # Внутренняя авторизация awg-ui → awg-ctrl: Ed25519-пара. Приватный → awg-ui, # публичный → awg-ctrl. Перегенерируется при каждой установке (эфемерна: обе # стороны переписываются вместе) — даже при KEEP_DATA, на vpn:// ключи не влияет. ( umask 077 openssl genpkey -algorithm ed25519 -out "$INTERNAL_AUTH_PRIV" openssl pkey -in "$INTERNAL_AUTH_PRIV" -pubout -out "$INTERNAL_AUTH_PUB" ) chmod 600 "$INTERNAL_AUTH_PRIV" "$INTERNAL_AUTH_PUB" ok "Ключи внутренней авторизации awg-ui ↔ awg-ctrl" grep -qxF 'net.ipv4.ip_forward=1' /etc/sysctl.conf \ || echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf grep -qxF 'net.ipv6.conf.all.forwarding=1' /etc/sysctl.conf \ || echo 'net.ipv6.conf.all.forwarding=1' >> /etc/sysctl.conf sysctl -qp ok "IP forwarding включён" cat > "$AWG_CONF" </dev/null; then warn "Интерфейс $IFACE уже существует — перезапускаем" awg-quick down "$IFACE" 2>/dev/null || true fi awg-quick up "$IFACE" || fail "awg-quick up $IFACE завершился с ошибкой" # awg-quick не всегда применяет приватный ключ из [Interface]/PostUp # (наблюдалось: awg show public-key = none сразу после up). Если ключ не # совпал — доставляем его явно из файла и проверяем ещё раз. RUNNING_PUB=$(awg show "$IFACE" public-key 2>/dev/null || echo "") if [[ "$RUNNING_PUB" != "$PUB_KEY" ]]; then awg set "$IFACE" private-key "$PRIV_KEY_FILE" \ || fail "Не удалось применить приватный ключ к $IFACE" RUNNING_PUB=$(awg show "$IFACE" public-key 2>/dev/null || echo "") fi if [[ "$RUNNING_PUB" == "$PUB_KEY" ]]; then ok "Интерфейс $IFACE запущен, ключ применён" else fail "Ключ на $IFACE не совпал: ${RUNNING_PUB:-none} ≠ $PUB_KEY" fi step "6/7 npm install" npm_install_all # Глобальный бинарник — используем tsx напрямую из node_modules (не npx) # чтобы child.pid в cli был реальным PID процесса cat > /usr/local/bin/awg-ctrl << 'WRAPPER' #!/usr/bin/env bash exec /opt/awg-control/cli/node_modules/.bin/tsx \ /opt/awg-control/cli/src/index.ts "$@" WRAPPER chmod +x /usr/local/bin/awg-ctrl ok "awg-ctrl → /usr/local/bin/awg-ctrl" step "7/7 Запуск сервисов" cat > "$PROJECT/cli/cli.env" <