diff --git a/Dockerfile b/Dockerfile index 2738da5..8e7686c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,8 @@ RUN mkdir -p /data && chmod 700 /data WORKDIR /app -COPY package.json ./ -RUN npm install --omit=dev +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev COPY server.js ./server.js COPY scripts ./scripts diff --git a/README.md b/README.md index 8cfedc8..d3fd6f7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,20 @@ curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/s Панель после установки открывается по **`http://IP:HOST_PORT`** (по умолчанию `:8080`). Первый вход: **логин `admin`, пароль `admin`**. Смените пароль сразу после входа. +### Обновить/переустановить панель с GitHub + +Если `/opt/amnezia-admin` не git-репозиторий, это нормально: установщик скачивает свежий архив с GitHub, делает backup старой папки, сохраняет текущий порт и текущий Docker volume `/data`, затем пересобирает только контейнер панели **`amnezia-admin`**. Контейнеры AmneziaWG (`amnezia-awg`, `amnezia-awg2` и т.д.) не удаляются и не пересоздаются. + +```bash +curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo bash +``` + +Если не нужен лендинг на порту 80: + +```bash +curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo env SKIP_LANDING=1 bash +``` + ### Переменные окружения и `sudo` Частая ошибка: задали **`AWG_PROFILES`**, **`ADMIN_PASSWORD`** или другую переменную из таблицы «в одной строке» перед `curl`, а затем выполнили **`curl … | sudo bash`**. По умолчанию **`sudo` не передаёт** это окружение в `bash`, который читает установочный скрипт с stdin: установщик **не увидит** переменные, переключатель **«Инстанс»** не появится. @@ -80,7 +94,7 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas | `DATA_DIR` | `/opt/amnezia-admin-data` | Том с `password.hash` и сессией | | `HOST_PORT` | `8080` | Порт HTTP панели на хосте | | `AWG_CONTAINER` | `amnezia-awg` | Имя контейнера Amnezia WG | -| `AWG_PROFILES` | _(нет)_ | JSON-массив профилей: несколько контейнеров/путей (см. ниже). Если задан — переключатель «Инстанс» в вебе. При однострочной установке с переменной в окружении нужен **`sudo -E bash`** или файл **`/root/amnezia-admin.awg-profiles.json`** — см. раздел **«Переменные окружения и sudo»** выше. В каждом объекте можно задать `warpDir`, `warpConf`, `warpClientsList`, `startScript` — см. раздел **Cloudflare WARP** | +| `AWG_PROFILES` | auto | JSON-массив профилей: несколько контейнеров/путей (см. ниже). Если не задан и на VPS уже есть несколько контейнеров `amnezia-awg*`, установщик сам соберёт профили и сохранит их в `/root/amnezia-admin.awg-profiles.json`. Если задан вручную — переключатель «Инстанс» в вебе. При однострочной установке с переменной в окружении нужен **`sudo -E bash`** или файл **`/root/amnezia-admin.awg-profiles.json`** — см. раздел **«Переменные окружения и sudo»** выше. В каждом объекте можно задать `warpDir`, `warpConf`, `warpClientsList`, `startScript` — см. раздел **Cloudflare WARP** | | `WARP_DIR` | `/opt/warp` | В контейнере AWG: каталог для `warp.conf` и `clients.list` | | `WARP_CONF_PATH` | `{WARP_DIR}/warp.conf` | Нестандартный путь к конфигу WARP | | `WARP_CLIENTS_LIST` | `{WARP_DIR}/clients.list` | Список строк `10.8.x.x/32` — кому маршрутизировать трафик через интерфейс `warp` | @@ -112,7 +126,7 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas | `MTPRO_PUBLISH_BIND` | `0.0.0.0` | Адрес bind на хосте для `-p` | | `MTPRO_PUBLIC_HOST` | _(нет)_ | Публичный IP/DNS для ссылки **`tg://proxy`**; можно вместо этого задать **`CLIENT_CONFIG_ENDPOINT`** | -Переменная **`AWG_PROFILES`** при установке автоматически сохраняется в **`/root/amnezia-admin.awg-profiles.json`** на VPS; при следующем запуске `install.sh` без `AWG_PROFILES` значение подставляется из этого файла или из **старого контейнера** `amnezia-admin` перед его удалением — так переключатель «Инстанс» не пропадает после обновления панели. +Переменная **`AWG_PROFILES`** при установке автоматически сохраняется в **`/root/amnezia-admin.awg-profiles.json`** на VPS; при следующем запуске `install.sh` без `AWG_PROFILES` значение подставляется из этого файла или из **старого контейнера** `amnezia-admin` перед его удалением — так переключатель «Инстанс» не пропадает после обновления панели. Если файла и переменной нет, но на VPS запущено несколько контейнеров **`amnezia-awg*`**, установщик сам создаёт профили по контейнерам. #### Несколько инстансов (AmneziaWG + Legacy и т.д.) @@ -136,7 +150,7 @@ curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/s Если в вебе пропал список **«Инстанс»**, смотрите раздел «Если пропал список Инстанс» в **[docs/panel-guide.md](docs/panel-guide.md)**. -Если на хосте уже запущено **несколько** контейнеров с именами вида **`amnezia-awg*`**, а в панели по-прежнему один инстанс — задайте **`AWG_PROFILES`** (или файл **`/root/amnezia-admin.awg-profiles.json`**) и перезапустите установщик; при запуске **`install.sh`** без профилей в этом случае выводится предупреждение в консоль. +Если на хосте уже запущено **несколько** контейнеров с именами вида **`amnezia-awg*`**, а в панели по-прежнему один инстанс — перезапустите установщик из GitHub. Он автоматически создаст **`AWG_PROFILES`** и сохранит файл **`/root/amnezia-admin.awg-profiles.json`**. Ручной JSON нужен только при нестандартных путях внутри контейнеров. ### Telegram MTProto‑прокси diff --git a/docs/panel-guide.md b/docs/panel-guide.md index e948a1e..829e9b6 100644 --- a/docs/panel-guide.md +++ b/docs/panel-guide.md @@ -6,7 +6,7 @@ | Блок в интерфейсе | Назначение | |-------------------|------------| -| **Инстанс** | Переключение между профилями из `AWG_PROFILES` (разные контейнеры AmneziaWG / Legacy). Виден только если в контейнере панели задан JSON из **двух и более** профилей. | +| **Инстанс** | Переключение между профилями `AWG_PROFILES` (разные контейнеры AmneziaWG / Legacy). Виден, если панель получила **два и более** профиля. Установщик сам создаёт профили, когда на VPS уже запущено несколько контейнеров `amnezia-awg*`. | | **Время** | Отображение часов контейнера и браузера; синхронизация времени хоста VPS по SSH (если доступно). | | **Cloudflare WARP** | *Необязательно.* Вывод части клиентов в интернет через интерфейс `warp` в контейнере AWG. Если WARP не ставили — статус **«Не установлен»** нормален; панель и VPN без этого работают. Установка и удаление — скрипт `scripts/warp-amnezia.sh` на хосте (`install` / **`uninstall`**), подробности в основном [README](../README.md). | | **Telegram MTProto‑прокси** | *Необязательно*, не часть AWG. Docker-контейнер официального образа **telegrammessenger/proxy**; установка из панели, ссылка **`tg://proxy`**, переменные **`MTPRO_*`** — см. основной [README](../README.md). | @@ -19,8 +19,13 @@ ```bash docker inspect amnezia-admin --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^AWG_PROFILES=' ``` -2. Если строки нет — один раз задайте JSON при установке (пример см. в основном [README](../README.md)) или восстановите файл **`/root/amnezia-admin.awg-profiles.json`** на VPS и снова запустите `install.sh` **без** своего `AWG_PROFILES` — установщик подставит значение из файла или из старого контейнера перед удалением. -3. После правок обновите страницу с **жёстким сбросом кэша** (Ctrl+Shift+R). +2. Если строки нет, но `docker ps` показывает два контейнера вида `amnezia-awg*`, переустановите панель свежим установщиком из GitHub: + ```bash + curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo env SKIP_LANDING=1 bash + ``` + Установщик сам создаст **`AWG_PROFILES`** и сохранит **`/root/amnezia-admin.awg-profiles.json`**. +3. Ручной JSON нужен только при нестандартных путях внутри AWG-контейнеров. +4. После правок обновите страницу с **жёстким сбросом кэша** (Ctrl+Shift+R). ## Лендинг не поднимается (порт 80 занят) diff --git a/package-lock.json b/package-lock.json index 6b0341f..bf8bcac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "amnezia-admin", - "version": "1.2.28", + "version": "1.2.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "amnezia-admin", - "version": "1.2.28", + "version": "1.2.29", "license": "MIT", "dependencies": { "express": "^4.21.2" diff --git a/package.json b/package.json index eca3a41..205a54d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amnezia-admin", - "version": "1.2.28", + "version": "1.2.29", "private": false, "description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)", "license": "MIT", diff --git a/scripts/install.sh b/scripts/install.sh index e8318ef..f0c9504 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -40,6 +40,29 @@ cleanup() { } trap cleanup EXIT +# Preserve current container settings before replacing source tree. This makes +# curl-based reinstall safe even when /opt/amnezia-admin is not a git checkout. +PREV_HOST_PORT="" +PREV_DATA_DIR="" +PREV_CONTAINER_ENV="" +if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then + __dock_port_out="" + __dock_port_out="$(docker port "${CONTAINER_NAME}" 3980/tcp 2>/dev/null)" || : + if [[ -n "${__dock_port_out}" ]]; then + PREV_HOST_PORT="$(printf '%s\n' "${__dock_port_out}" | head -n1 | awk -F: '{print $NF}')" || PREV_HOST_PORT="" + fi + PREV_DATA_DIR="$(docker inspect "${CONTAINER_NAME}" --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Source}}{{end}}{{end}}' 2>/dev/null || true)" + PREV_CONTAINER_ENV="$(docker inspect "${CONTAINER_NAME}" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null || true)" +fi +if [[ -n "${PREV_HOST_PORT}" && "${HOST_PORT}" == "8080" ]]; then + HOST_PORT="${PREV_HOST_PORT}" + echo "→ Уже запущен ${CONTAINER_NAME}: сохраняю внешний порт ${HOST_PORT} (укажите HOST_PORT=… чтобы сменить)." +fi +if [[ -n "${PREV_DATA_DIR}" && "${DATA_DIR}" == "/opt/amnezia-admin-data" ]]; then + DATA_DIR="${PREV_DATA_DIR}" + echo "→ Уже запущен ${CONTAINER_NAME}: сохраняю DATA_DIR ${DATA_DIR}." +fi + if [[ "${SKIP_DOWNLOAD:-}" != "1" ]]; then echo "→ Клонирование релиза ${GITHUB_REPO} (${BRANCH})..." echo "→ Скачивание tar.gz с GitHub (вывода может не быть 1–10 мин.; при блокировках задайте зеркало GITHUB_REPO_URL_OVERRIDE или см. CURL_MAX_TIME ниже)." @@ -72,6 +95,11 @@ if [[ "${SKIP_DOWNLOAD:-}" != "1" ]]; then echo "Ошибка: распакованный архив пуст (${CURL_URL})." exit 1 fi + if [[ -d "${INSTALL_DIR}" && "${AUTO_BACKUP_INSTALL:-1}" != "0" ]]; then + __install_backup="${INSTALL_DIR}.backup.$(date -u +%Y%m%dT%H%M%SZ)" + cp -a "${INSTALL_DIR}" "${__install_backup}" 2>/dev/null || true + echo "→ Backup старых исходников: ${__install_backup}" + fi rm -rf "${INSTALL_DIR}" mkdir -p "$(dirname "${INSTALL_DIR}")" mv "${__extracted_dir}" "${INSTALL_DIR}" @@ -82,21 +110,6 @@ fi mkdir -p "${DATA_DIR}" mkdir -p /opt/amnezia-instances -# При повторном запуске не менять внешний порт панели, если не указали HOST_PORT явно (по умолчанию 8080). -PREV_HOST_PORT="" -if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then - # Контейнер может быть остановлен — `docker port` тогда код ≠ 0; при pipefail без этого блока скрипт выходит молча. - __dock_port_out="" - __dock_port_out="$(docker port "${CONTAINER_NAME}" 3980/tcp 2>/dev/null)" || : - if [[ -n "${__dock_port_out}" ]]; then - PREV_HOST_PORT="$(printf '%s\n' "${__dock_port_out}" | head -n1 | awk -F: '{print $NF}')" || PREV_HOST_PORT="" - fi - if [[ -n "${PREV_HOST_PORT}" && "${HOST_PORT}" == "8080" ]]; then - HOST_PORT="${PREV_HOST_PORT}" - echo "→ Уже запущен ${CONTAINER_NAME}: сохраняю внешний порт ${HOST_PORT} (укажите HOST_PORT=… чтобы сменить)." - fi -fi - BOOT_PW="" PASS_FILE="/root/amnezia-admin.initial-password" if [[ -n "${ADMIN_PASSWORD:-}" ]]; then @@ -124,14 +137,14 @@ AWG_PROFILE_SNAPSHOT="/root/amnezia-admin.awg-profiles.json" if [[ -n "${AWG_PROFILES:-}" ]]; then umask 077 printf '%s\n' "${AWG_PROFILES}" >"${AWG_PROFILE_SNAPSHOT}" 2>/dev/null || true -elif docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then +elif [[ -n "${PREV_CONTAINER_ENV}" ]]; then PREV_AWG_PROFILES="" while IFS= read -r __env_line; do if [[ "${__env_line}" == AWG_PROFILES=* ]]; then PREV_AWG_PROFILES="${__env_line#AWG_PROFILES=}" break fi - done < <(docker inspect "${CONTAINER_NAME}" --format '{{range .Config.Env}}{{println .}}{{end}}') + done <<<"${PREV_CONTAINER_ENV}" if [[ -n "${PREV_AWG_PROFILES}" ]]; then AWG_PROFILES="${PREV_AWG_PROFILES}" echo "→ AWG_PROFILES восстановлен из предыдущего контейнера ${CONTAINER_NAME}." @@ -158,20 +171,34 @@ if [[ -z "${AWG_CONTAINER:-}" ]] && [[ -z "${AWG_PROFILES:-}" ]]; then fi if [[ -z "${AWG_PROFILES:-}" ]]; then - __awg_multi_count="$( - docker ps --format '{{.Names}}' 2>/dev/null | awk '/^amnezia-awg/ { c++ } END { print c + 0 }' | tr -d '[:space:]' - )" - if [[ "${__awg_multi_count:-0}" =~ ^[0-9]+$ ]] && [[ "${__awg_multi_count}" -gt 1 ]]; then - echo "⚠ Запущено ${__awg_multi_count} контейнеров с именами amnezia-awg*, но AWG_PROFILES не задан." - echo " Переключатель «Инстанс» в панели не появится: см. README, раздел «Несколько инстансов» и «Переменные окружения и sudo»." - echo " Без sudo -E: запишите JSON одной строкой в ${AWG_PROFILE_SNAPSHOT} и снова запустите этот установщик." + __awg_names="$(docker ps --format '{{.Names}}' 2>/dev/null | awk '/^amnezia-awg/ { print }' | sort || true)" + __awg_multi_count="$(printf '%s\n' "${__awg_names}" | sed '/^$/d' | wc -l | tr -d '[:space:]')" + if [[ "${__awg_multi_count:-0}" =~ ^[0-9]+$ ]] && [[ "${__awg_multi_count}" -eq 1 ]] && [[ -z "${AWG_CONTAINER:-}" ]]; then + AWG_CONTAINER="$(printf '%s\n' "${__awg_names}" | sed '/^$/d' | head -n1)" + echo "→ AWG_CONTAINER авто-выбран: ${AWG_CONTAINER}" + elif [[ "${__awg_multi_count:-0}" =~ ^[0-9]+$ ]] && [[ "${__awg_multi_count}" -gt 1 ]]; then + __awg_profiles="[" + __sep="" + while IFS= read -r __awg_name; do + [[ -z "${__awg_name}" ]] && continue + __id="${__awg_name#amnezia-}" + __label="AmneziaWG" + if [[ "${__awg_name}" == "amnezia-awg2" ]]; then + __label="AmneziaWG 2.0" + elif [[ "${__awg_name}" != "amnezia-awg" ]]; then + __label="AmneziaWG ${__id}" + fi + __awg_profiles+="${__sep}{\"id\":\"${__id}\",\"label\":\"${__label}\",\"container\":\"${__awg_name}\",\"confPath\":\"/opt/amnezia/awg/awg0.conf\",\"clientsPath\":\"/opt/amnezia/awg/clientsTable\",\"iface\":\"awg0\",\"wgBinary\":\"awg\",\"pskPath\":\"/opt/amnezia/awg/wireguard_psk.key\"}" + __sep="," + done <<<"${__awg_names}" + __awg_profiles+="]" + AWG_PROFILES="${__awg_profiles}" + umask 077 + printf '%s\n' "${AWG_PROFILES}" >"${AWG_PROFILE_SNAPSHOT}" 2>/dev/null || true + echo "→ AWG_PROFILES авто-собран из контейнеров amnezia-awg*: ${AWG_PROFILE_SNAPSHOT}" fi fi -PREV_CONTAINER_ENV="" -if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then - PREV_CONTAINER_ENV="$(docker inspect "${CONTAINER_NAME}" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null || true)" -fi for __ui_var in UI_HIDE_SECTIONS UI_HIDE_USERS UI_HIDE_WARP UI_HIDE_CASCADE UI_HIDE_MTPROTO WARP_SSH_INSTALL_DIR; do if [[ -z "${!__ui_var:-}" ]] && [[ -n "${PREV_CONTAINER_ENV}" ]]; then PREV_VAL=""