46 Commits

Author SHA1 Message Date
Бобырев Андрей
b64d1aba5b v1.4.2: move to git.bobyrev.com (GitHub account suspended), README photo + install command
Some checks failed
Publish Pro image to GHCR / build-push (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 04:39:35 +03:00
Бобырев Андрей
cb230fe50e docs: split old Notes into 09.3.N, fix 00 (news bot), move check scripts to scripts/checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:44:20 +03:00
Бобырев Андрей
dbab96f7fd docs: 09.2 — install.sh checked on clean Ubuntu 24.04 LXC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:36:48 +03:00
Бобырев Андрей
d34f25d544 docs: 09.2 v1.4.1, news, gitea
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:25:57 +03:00
Бобырев Андрей
1be56c1f60 v1.4.1: installer installs Docker, step-by-step quick start, drop confusing AWG_PROFILES hint
Some checks failed
Publish Pro image to GHCR / build-push (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:20:11 +03:00
Бобырев Андрей
a843f300a7 docs: 00, 09.2 v1.4.0, archive closed panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:09:32 +03:00
Бобырев Андрей
74849a8e2c v1.4.0: Xray VLESS-Reality, fully open panel (no community edition)
Some checks failed
Publish Pro image to GHCR / build-push (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:08:09 +03:00
Бобырев Андрей
693f9513f1 docs: 09.1 news posted 2026-10-23 00:55:23 +03:00
Бобырев Андрей
2faa91de9b docs: 09.1 v1.3.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 00:43:09 +03:00
Бобырев Андрей
75345ebc03 v1.3.0: AWG 3.1/3.0/2.0/1.x/WireGuard from the panel, real cascade, server domain, light/dark theme
Some checks failed
Publish Pro image to GHCR / build-push (push) Has been cancelled
- client .conf: only obfuscation keys present on the server (AWG 1.x got S3/S4/I1 -> no traffic)
- install.sh no longer puts panel-managed containers into AWG_PROFILES (wrong instance on switch)
- UDP port busy check via docker instead of ss inside the panel container
- default profile never hijacks a managed container

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 00:42:16 +03:00
Бобырев Андрей
6e05a366c9 docs: add PROJECT_INDEX.md
Указатель проекта по регламенту синхронизации: пути, состав репозитория,
порты и переменные, известные грабли, текущее состояние.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:26:00 +03:00
Бобырев Андрей
7b92bfb852 fix: remove stale landing container when SKIP_LANDING=1
Контейнер лендинга запускается с --restart unless-stopped и переживает
переустановку. При SKIP_LANDING=1 скрипт новый не поднимал, но старый
продолжал держать порт 80, и лендинг «всё равно открывался».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:23:54 +03:00
34415a4b8a fix: keep disconnect dates in the clients metadata snapshot
Даты «Задать дату» (lastDisconnectedAt) и отключение по расписанию
(scheduledTunnelDisconnectAt) тоже живут в clientsTable и терялись при
затирании. Снимок хранит и их, но возвращает только когда запись реально
затёрли (пропал creationDate) — иначе воскресало бы снятое в панели расписание.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 11:40:14 +03:00
194ac8c742 fix: keep client names and dates when Amnezia app rewrites clientsTable
Приложение AmneziaVPN при переподключении пересобирает clientsTable из awg0.conf
и оставляет только clientId с именами «Client N» — имена, даты создания и last_config
пропадают. Панель теперь держит снимок этих полей в DATA_DIR и восстанавливает их
при следующем чтении состояния, попутно чиня файл в контейнере.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 02:10:36 +03:00
andrey271192
8e5d51ab2c fix: auto-detect wg binary in containers 2026-06-22 15:46:43 +03:00
andrey271192
b3600959ef fix: auto-discover amnezia client sources 2026-06-22 00:34:12 +03:00
andrey271192
4e6ab2dd6a fix: parse legacy clients table formats 2026-06-21 19:29:08 +03:00
andrey271192
dfeabe02d7 feat: sync app-created peers into clients table 2026-06-21 19:19:49 +03:00
andrey271192
fd66b0a024 fix: show peers missing clients table 2026-06-21 19:16:01 +03:00
andrey271192
cb5fca3d0f fix: load clients for selected profile 2026-06-21 19:00:51 +03:00
andrey271192
a9a67afb54 fix: show distinct awg instance labels 2026-06-21 18:34:26 +03:00
andrey271192
8c67d7df93 fix: dedupe duplicate awg instances 2026-06-21 18:32:10 +03:00
andrey271192
cff27e27b6 feat: import clients from amnezia configs 2026-06-21 18:22:36 +03:00
andrey271192
8ba2a266e5 fix: refresh stale awg profile snapshots 2026-06-21 18:14:02 +03:00
andrey271192
cbc20a1b71 fix: auto-detect awg profiles on install 2026-06-21 17:53:58 +03:00
andrey271192
1960c2212a feat(install): AmneziaWG presence check + clear warnings/logs
Addresses tester feedback: the installer gave no signal about whether
a working AmneziaWG server was present, so failures ("cannot create
client") were hard to diagnose.

install.sh now prints a "Проверка AmneziaWG" block after deploy:
- lists every container that actually holds /opt/amnezia/awg/*.conf,
  with its UDP port and client count;
- states that the panel auto-discovers them (no manual AWG_PROFILES);
- if none found, warns loudly that this is normal on a fresh VPS and
  spells out the two ways forward (deploy an instance from the panel's
  "Протоколы / инстансы", or install a server with the Amnezia app),
  noting that until then the client list is empty by design.
2026-06-16 21:46:49 +03:00
andrey271192
cdd47f157b feat(instances): show app-installed AmneziaWG, not only panel-created
The instances section listed only panel-managed instances, so an
AmneziaWG container set up by the Amnezia desktop app (e.g.
amnezia-awg2) did not appear as a card.

/api/instances now iterates ALL effective profiles (env-discovered +
managed): resolves each container, reads ListenPort and peer count,
and detects the variant from the interface config (S3/S4 -> awg2,
S1/S2 -> awg, wg0 -> legacy). Native (non-managed) instances are
flagged managed:false.

UI: native cards get an "приложение Amnezia" tag and hide the Удалить
button (the panel must not tear down app-managed containers); they
keep Подключения and Стоп/Старт. Stop/start now resolve the real
container name so they work for native instances too.

Verified live: native amnezia-awg2 shows as "AmneziaWG 2.0" port
37395 (managed:false) alongside a panel-created Legacy :5534.
2026-06-16 21:40:42 +03:00
andrey271192
546c1127f8 feat: deploy AmneziaWG instances from the panel (no Amnezia app)
Add a "Протоколы / инстансы" section that spins up AmneziaWG server
containers straight from the panel, with port + variant selection —
no need to run the Amnezia desktop app to set up the server.

Core (scripts/awg-instance.sh):
- create <awg2|awg|legacy> <port> [name]: pulls the public image
  (amneziavpn/amneziawg-go:2.0.0 / :0.2.18 / amneziavpn/amnezia-wg),
  generates server keys + psk, writes awg0.conf/wg0.conf with a free
  10.8.<N>.0/24 subnet (scans running containers to avoid clashes),
  random AmneziaWG obfuscation (Jc/Jmin/Jmax/S1..S4/H1..H4 as single
  uint32 values — ranges break awg setconf), and a start.sh that
  brings the iface up via userspace amneziawg-go + NAT MASQUERADE.
  remove <name>, list.

Backend (server.js):
- Profiles are now dynamic: env AWG_PROFILES merged with managed
  instances persisted in /data/instances.json (getProfiles()), so a
  new instance is usable immediately without restarting the panel.
- /api/instances (list with running/peers), /api/instances/create,
  /delete, /stop, /start. create runs the script then registers the
  profile; delete tears down container + data + profile.

Infra:
- Dockerfile: add bash iproute2 coreutils, COPY scripts.
- install.sh: mkdir /opt/amnezia-instances and bind-mount it into the
  panel so docker-in-docker bind paths line up.

UI (index.html/app.js/styles.css):
- Cards per instance (icon, NEW badge, description, РАБОТАЕТ/ОСТАНОВЛЕН,
  port, connections) with Стоп/Старт, Подключения (switches the active
  instance), Удалить; plus a create form (variant + port).

Verified end to end on a live VPS: create awg2/awg/legacy instances,
interfaces come up, a client created on a new instance gets the right
subnet (10.8.20.2) and Endpoint (host:51850).
2026-06-16 21:33:02 +03:00
andrey271192
f1b7fa522c fix: runtime auto-discovery of AmneziaWG container
Tester hit 'No such container: amnezia-awg' after a stock install
on a VPS where Amnezia created the WG container as amnezia-awg2.
Install-time detection was not enough.

The panel now resolves the real container at runtime: if the
configured AWG_CONTAINER lacks the conf file (or does not exist),
it scans running containers for one holding awg0.conf, preferring
amnezia-awg* names. All docker exec paths (clients list/create/
enable/disable/delete, WARP, host-setup) and the API container
field use the resolved name. Clear error lists running containers
if none match.

Result: install -> login -> change password -> create client works
with no manual AWG_CONTAINER/AWG_PROFILES on single-instance setups.
2026-06-16 21:11:36 +03:00
andrey271192
e498e98e12 fix(export): Amnezia-standard DNS + drop empty I2-I5 junk lines
Two issues reported on direct/cascade client creation:
- DNS hardcoded to Google (8.8.8.8/8.8.4.4). Default now 1.1.1.1/
  1.0.0.1 (Amnezia standard); still overridable via
  CLIENT_EXPORT_DNS1/DNS2 env.
- Config emitted empty I2 =/I3 =/I4 =/I5 = lines when the server
  only defines I1. Now only non-empty I-params are written, so the
  client's AmneziaWG obfuscation matches the server head exactly
  (mismatched/empty junk lines could break some clients).
2026-06-16 20:10:00 +03:00
andrey271192
2899bba55f style: compact icon action buttons, no horizontal scroll
Replace text action buttons with 32px icon buttons (tooltips kept
via title/aria-label): ⏻ выключить, ▶ включить, ⬇ .conf,
🔗 прямая ссылка, ⧉ копировать URL, 🗑 удалить. Actions cell
shrinks to content (width:1%, nowrap) so the table fits without
left-right scrolling.
2026-06-16 19:57:49 +03:00
andrey271192
4ebb81cede style: client action buttons flow in a row, not a column
Widen actions cell (min-width 360px) and keep buttons nowrap +
no-shrink so Выключить/Скачать/Прямая ссылка/Копировать/Удалить
wrap horizontally instead of stacking one per line.
2026-06-16 19:50:54 +03:00
andrey271192
c49e7eea8d feat: direct client creation + auto-detect AWG container
Add "Новый клиент" (direct) flow alongside cascade:
- server.js: POST /api/clients/create — Endpoint = this server's
  public IP:ListenPort (CLIENT_CONFIG_ENDPOINT or request host),
  reuses the same key/peer/conf pipeline as create-cascade.
- public/index.html: "Новый клиент" panel with #direct-form
  (name + optional tunnel IP).
- public/app.js: downloadDirectConf() handler + form binding.

install.sh hardening so fresh installs work out of the box:
- Auto-detect a single amnezia-awg* container (e.g. amnezia-awg2,
  Amnezia AWG 2.0 default) -> AWG_CONTAINER, instead of the fixed
  "amnezia-awg" default that mismatched and blocked client ops.
- Auto-default CLIENT_CONFIG_ENDPOINT to the host primary IP so
  direct .conf exports get a correct Endpoint without manual env.

Fixes "cannot create users" on servers whose WG container is
named amnezia-awg2.
2026-06-16 19:30:11 +03:00
Андрей Бобырев
c202f0bda5 chore: add npm lockfile 2026-06-07 03:05:26 +03:00
Андрей Бобырев
d21419cfdb fix: default awg container name 2026-06-06 14:46:37 +03:00
Андрей Бобырев
05e6d007e2 docs: make pro repo public install source 2026-06-06 14:16:56 +03:00
Андрей Бобырев
ec65bbc59a docs: move support block to bottom of README 2026-05-15 06:05:56 +03:00
Андрей Бобырев
038020408a feat: donate links on login screen + unified README support block
- public/index.html: gate-donate nav with Boosty/Ozon/Telegram on login
- public/styles.css: .gate-donate styles
- landing: proj-support-strip
- README: blockquote + "Поддержка проекта" section
- .github/FUNDING.yml: added
- version bumped to 1.2.28

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 05:52:17 +03:00
Андрей Бобырев
b212413e68 docs(readme): note screenshots match deploy bundle
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 05:06:26 +03:00
Андрей Бобырев
e9583ac4f1 docs(readme): PRO install via deploy repo, sources path separate
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 05:01:39 +03:00
Андрей Бобырев
9e5ed8a1e7 docs(readme): drop PAT install block, single curl line
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 04:58:28 +03:00
Андрей Бобырев
4e8f82616d docs(readme): private repo install PAT + Bearer on raw
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 04:55:05 +03:00
Андрей Бобырев
df1f9dc826 docs(ui): human cascade guide + kaskad_web_vpn
Expand cascade fold with steps, DNAT primer, install one-liner, link to github.com/andrey271192/kaskad_web_vpn; README and panel-guide aligned.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 04:49:14 +03:00
Андрей Бобырев
0e7a7df636 feat(mtproto): Telegram proxy panel and /api routes
- Docker telegrammessenger/proxy: install/restart/remove from UI
- GET/POST /api/mtproto/*, UI_HIDE_MTPROTO / UI_HIDE_SECTIONS=mtproto
- install.sh vars MTPRO_*; /health returns version; docs

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 04:39:19 +03:00
Андрей Бобырев
845683b986 fix(install): auth tarball download with GITHUB_TOKEN
Private repos return 404 on archive/refs/heads/<branch>.tar.gz
without Authorization. Use api.github.com/.../tarball/<ref> when
GITHUB_TOKEN/GH_TOKEN is set; tolerate the SHA-suffixed top dir.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 02:46:55 +03:00
Андрей Бобырев
39a74e5f0a fix(install): align with amnezia_web main (curl opts, port, AWG count)
Avoid pipefail exits on stopped container and zero AWG grep matches;
add curl timeouts and tarball override for flaky GitHub.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 02:00:50 +03:00
42 changed files with 5302 additions and 598 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,5 +1,6 @@
# Как в https://github.com/andrey271192/domen_hydra/blob/main/.github/FUNDING.yml
github: andrey271192
custom:
- https://boosty.to/andrey27/donate
- https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798
- https://t.me/lot_andrey

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ npm-debug.log*
*.swp
.env
.env.*
Новости/

View File

@@ -0,0 +1,10 @@
# 00. НАЧНИ ОТСЮДА — amnezia_web-PRO
**Промпт для нового чата:**
> Проект «Открытые проекты/amnezia_web-PRO» — полностью открытая веб-панель Amnezia (Node, `server.js` + `public/`). Закрытой версии нет (старая — в `Архив/amnezia_web-PRO_test`). Прочитай `PROJECT_INDEX.md` и `09. История и грабли`. Тесты — на school-friends (сервер) и school-it-kruzhok (клиент) через svault. Релиз: тег + GitHub API, затем Gitea, Notes, новость в группу PCA, тема «Новости» (chat_id -1003902709196, message_thread_id=131) ботом @PCAdmin27_bot (токен `TELEGRAM_MOD_BOT_TOKEN` в «Закрытые продукты/home_Friends_It/ДОСТУПЫ.md»); PCA_jarvis_bot не использовать. Gitea — pull-зеркало, обновлять mirror-sync.
## Оглавление
- `PROJECT_INDEX.md` — что где лежит, установка
- `09. История и грабли` — оглавление записей 09.N
- `scripts/checks/` — проверки парка AWG (запускать внутри контейнера панели, читают /app/data/data.json)
- `Новости/` — тексты новостей релизов (не в git)

View File

@@ -0,0 +1,6 @@
# 09. История и грабли
- [09.1 Версия 1.3.0 — протоколы с сайта, каскад, домен, тема](<09.1 Версия 1.3.0 — протоколы с сайта, каскад, домен, тема.md>) — 23.10.2026
- [09.2 Версия 1.4.0 — Xray VLESS, всё открыто](<09.2 Версия 1.4.0 — Xray VLESS, всё открыто.md>) — 23.10.2026
- [09.3 Старые исправления июня 2026](<09.3 Старые исправления июня 2026.md>) — 13 частей 09.3.N, перенесены из Notes 23.10.2026
- 04.10.2026 — проект переехал в папку-группу; код не менялся.

View File

@@ -0,0 +1,13 @@
# 09.1 Версия 1.3.0 — протоколы с сайта, каскад, домен, тема (23.10.2026)
СИМПТОМ: (а) клиент AWG 1.x из панели — рукопожатие есть, интернета нет; (б) после обновления переключение инстанса создавало клиента не в том контейнере; (в) проверка занятого порта ничего не ловила.
ПРИЧИНА: (а) выгрузка .conf подставляла «умолчательные» S3/S4/I1, которых нет на сервере 1.x; (б) install.sh клал контейнеры панели в AWG_PROFILES, env-копия затеняла управляемый инстанс; (в) `ss` запускался внутри контейнера панели.
КАК ПРОВЕРИТЬ ЗА МИНУТУ:
curl -s http://IP:8080/health # version 1.3.0
в .conf клиента 1.x не должно быть строк S3/S4; GET /api/protocols — id управляемых = amnezia-<вариант>-<порт>
ИСПРАВЛЕНИЕ: v1.3.0 (тег v1.3.0). Параметры обфускации берутся с сервера (awgObfuscationLines); getProfiles отдаёт приоритет управляемым; порт — через docker ps. Добавлено: AWG 3.0/3.1 (образы по дайджесту, патч awg-quick на userspace), домен в /data/settings.json, каскад POST /api/cascade/upstream (конфиг другого сервера → интерфейс warp), светлая тема.
Проверено на школе (friends = сервер, it-kruzhok = клиент/выход): Legacy, 1.x, 2.0, 3.0, 3.1 — рукопожатие + интернет; каскад — трафик прошёл через второй сервер, переживает рестарт; домен; обновление поверх контейнера приложения amnezia-awg; чистая установка с GitHub. Тестовое удалено.
ПРОФИЛАКТИКА: перед релизом гонять клиентов всех вариантов со второй машины (скрипт-клиент в контейнере amneziawg-go 3.1). Xray/VLESS в открытую панель не вошёл — есть в закрытом amnezia_web-PRO_test.
Не сделано: Gitea (.153:3030 недоступна из сети мини 23.10) — допушить `git push gitea main --tags`.
- 23.10.2026: новость о 1.3.0 опубликована в @PCAdministration (сообщение 877). Gitea не отправлено: Proxmox 192.168.88.5 и ВМ .153 не в сети — после включения `git push gitea main --tags`.

View File

@@ -0,0 +1,33 @@
# 09.2 Версия 1.4.0 — Xray VLESS-Reality, всё открыто (23.10.2026)
**Задача.** Перенести Xray/VLESS из закрытой панели, закрытую версию не развивать, убрать всё «платное».
**Сделано.**
- Убрана редакция community («только просмотр», баннер «купите PRO», `requireProTier`, `AMNEZIA_EDITION`, `COMMUNITY_UPGRADE_*`). Все функции открыты.
- Xray VLESS-Reality в `server.js` (блок «Xray VLESS-Reality») и раздел на странице: установка (TCP-порт, сайт-маскировка), клиенты создать/выкл/вкл/удалить, ссылка `vless://`.
Образ собирается на сервере (alpine + Xray v26.3.27), контейнер `amnezia-xray`, файлы `/opt/amnezia/xray` в формате приложения Amnezia (`xray_*.key`, `clientsTable`). Выключенный клиент — убран из `server.json`, помечен `disabled` в `clientsTable`.
- Закрытая панель `amnezia_web-PRO_test` перенесена в `Архив/` (SSD и Notes), там файл «АРХИВ — почему».
**Проверено** на school-friends (сервер) + school-it-kruzhok (клиент xray-core): соединение есть (HTTP 200 через socks), выключение клиента рвёт связь, включение возвращает. Тестовое удалено.
## Грабли
- **СИМПТОМ:** установка Xray «Не удалось получить ключи x25519». **ПРИЧИНА:** новый Xray пишет `Password (PublicKey): …` вместо `Public key:`. **ИСПРАВЛЕНИЕ:** регэксп `(?:Public\s*key|Password)[^:\n]*:`. **ПРОВЕРИТЬ:** `docker run --rm --entrypoint /usr/bin/xray amnezia-xray x25519`.
- **СИМПТОМ:** форма установки видна при установленном Xray. **ПРИЧИНА:** CSS `.instance-form` перебивает атрибут `hidden`. **ИСПРАВЛЕНИЕ:** `style.display`.
- Оба школьных сервера выходят в интернет через один NAT — проверка «по IP» не отличает туннель; проверять выключением клиента.
## 1.4.1 (23.10.2026)
- Установщик сам ставит Docker (`get.docker.com`) на чистом сервере. На машине без Docker не проверялось — школьные все с Docker.
- README: раздел «Быстрый старт» — 7 шагов от пустого VPS (порты, вход admin, протокол, клиент, Xray, домен, «если не работает»).
- Убрана подсказка «задайте AWG_PROFILES» — сбивала новичков.
- Проверено как у человека с GitHub: чистая установка `curl … | sudo bash` → вход admin → AWG 2.0 + клиент (рукопожатие, интернет) → Xray + клиент → обновление поверх (версия 1.4.1, клиенты на месте). Тестовое удалено.
## Проверка установщика на чистом сервере (23.10.2026)
- LXC 199 на Proxmox .5 (Ubuntu 24.04, unprivileged, nesting+keyctl), без Docker → `curl -fsSL …/scripts/install.sh | bash` → Docker поставился сам, контейнеры `amnezia-admin` и `amnezia-web-landing` поднялись, :8080 = 200, `/api/login` с паролем admin = ok. Ошибок нет, 1.4.2 не понадобилась. LXC удалён.
- Proxmox .5 и Gitea .153:3030 из сессии отвечают напрямую; в Gitea тег v1.4.1 есть.
## Публикация
- Новость 1.4.0 — @PCAdmin27_bot, тема «Новости» (131), сообщение 881. PCA_jarvis_bot из группы выгнан.
- Gitea — pull-зеркало: обновляется `mirror-sync` (push → 403). С мини .153 «недоступна» только из сессии Claude — ходить через `ssh andrey@127.0.0.1`.
## Осталось
- QR-кода для vless нет (ссылка копируется) — добавить, если попросят.

View File

@@ -0,0 +1,17 @@
# 09.3 Старые исправления июня 2026
Оглавление частей: заметки из Notes до регламента (июнь–июль 2026), по одной на исправление.
- [09.3.1 Canonical public amnezia_web-PRO 06.06](<09.3.1 Canonical public amnezia_web-PRO 06.06.md>)
- [09.3.2 Canonical amnezia_web-PRO status 21.06](<09.3.2 Canonical amnezia_web-PRO status 21.06.md>)
- [09.3.3 Fix AWG auto profiles 21.06](<09.3.3 Fix AWG auto profiles 21.06.md>)
- [09.3.4 Fix stale AWG profile snapshot 21.06](<09.3.4 Fix stale AWG profile snapshot 21.06.md>)
- [09.3.5 Client import from Amnezia backup 21.06](<09.3.5 Client import from Amnezia backup 21.06.md>)
- [09.3.6 Fix duplicate AWG instances 21.06](<09.3.6 Fix duplicate AWG instances 21.06.md>)
- [09.3.7 Fix AWG instance labels 21.06](<09.3.7 Fix AWG instance labels 21.06.md>)
- [09.3.8 Fix clients selected profile 21.06](<09.3.8 Fix clients selected profile 21.06.md>)
- [09.3.9 Fix peers missing clientsTable 21.06](<09.3.9 Fix peers missing clientsTable 21.06.md>)
- [09.3.10 Sync app-created Amnezia users 21.06](<09.3.10 Sync app-created Amnezia users 21.06.md>)
- [09.3.11 Fix legacy clientsTable formats 21.06](<09.3.11 Fix legacy clientsTable formats 21.06.md>)
- [09.3.12 Fix Amnezia source auto-discovery 22.06](<09.3.12 Fix Amnezia source auto-discovery 22.06.md>)
- [09.3.13 Fix awg binary not found 22.06](<09.3.13 Fix awg binary not found 22.06.md>)

View File

@@ -0,0 +1,17 @@
# 09.3.1 Canonical public amnezia_web-PRO 06.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
amnezia_web-PRO canonical public repo — 06.06
Keep one public project: https://github.com/andrey271192/amnezia_web-PRO
Install
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo bash
Defaults
Fresh login: admin
Fresh password: admin
Change password after first login.
GitHub
Repo is public.
Private download flow removed.
Separate deploy bundle removed from active docs and notes.
Commit: 05e6d00 docs: make pro repo public install source.

View File

@@ -0,0 +1,23 @@
# 09.3.10 Sync app-created Amnezia users 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Commit: dfeabe0 feat: sync app-created peers into clients table
Version: v1.2.36
Задача
Клиенты уже были созданы в приложении Amnezia. Их нужно подтянуть в веб-панель, а не создавать заново.
Решение
Добавлена кнопка Пользователи → Подтянуть из awg0.conf.
POST /api/clients/sync-peers берёт активные [Peer] текущего инстанса и создаёт недостающие строки clientsTable.
VPN-конфиг не пересоздаётся, контейнеры клиентов не трогаются.
Имена создаются как Клиент IP или Peer KEY, потом можно переименовать.
Экспорт .conf появится после импорта клиентского .conf, потому приватный ключ клиента есть только в приложении.
Команда
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
Потом Ctrl+F5 → выбрать Инстанс → Пользователи → Подтянуть из awg0.conf.
Проверено
node --check server.js
node --check public/app.js
bash -n scripts/*.sh
npm audit --audit-level=moderate: 0 vulnerabilities
git diff --check

View File

@@ -0,0 +1,17 @@
# 09.3.11 Fix legacy clientsTable formats 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Commit: 4e6ab2d fix: parse legacy clients table formats
Version: v1.2.37
Причина
Старые клиенты могли не подтягиваться, если clientsTable не был массивом [{clientId,userData}], а имел object/map/nested формат другой версии Amnezia.
Исправлено
parseClientsTable поддерживает array, object map, nested clients/users/rows/items/data/clientList/clientsTable.
clientRowId понимает clientId/id/publicKey/public_key/key/client_id и вложенные userData поля.
Добавлен GET /api/clients/source-report для диагностики источников без полных ключей.
Команда
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
Потом Ctrl+F5 → выбрать Инстанс → Пользователи → Подтянуть из awg0.conf.
Если всё равно пусто
Открыть в браузере после логина: /api/clients/source-report?profileId=awg и /api/clients/source-report?profileId=awg2

View File

@@ -0,0 +1,20 @@
# 09.3.12 Fix Amnezia source auto-discovery 22.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Commit: b360095 fix: auto-discover amnezia client sources
Version: v1.2.38
Root cause
Amnezia installs can keep old clients in /opt/amnezia/awg/wg0.conf, while profile pointed to /opt/amnezia/awg/awg0.conf. clientsTable can also live in /opt/amnezia/wireguard/clientsTable or have non-array format.
Fix
Runtime now scans config candidates: awg0.conf, wg0.conf, wireguard/wg0.conf.
Chooses config with max [Peer] count.
Runtime scans clientsTable candidates: awg/clientsTable and wireguard/clientsTable.
Chooses clientsTable with max parsed client count.
iface is inferred from selected config filename.
source-report shows resolved confPath/clientsPath/iface.
Command
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
Then Ctrl+F5 → select Instance → Users → Pull from awg0.conf.
Source
Amnezia issue #1615 mentions manual flow with /opt/amnezia/awg/wg0.conf and clientsTable; issue #2238 confirms clientsTable field variance.

View File

@@ -0,0 +1,15 @@
# 09.3.13 Fix awg binary not found 22.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Commit: 8e5d51a fix: auto-detect wg binary in containers
Version: v1.2.39
Symptom
При добавлении/подтягивании старых .conf ошибка: sh: awg: not found. В amneziawg контейнере awg binary отсутствует.
Fix
Runtime auto-detects binary inside container: preferred wgBinary, then wg for wg0.conf, then awg, then wg.
show/pubkey/genkey/syncconf use resolved binary, not hardcoded profile.wgBinary.
source-report and /api/clients expose resolved wgBinary.
Command
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
Then Ctrl+F5 → Instance → Users → Pull from awg0.conf.

View File

@@ -0,0 +1,20 @@
# 09.3.2 Canonical amnezia_web-PRO status 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Важно: amnezia_web-PRO_test — отдельный продукт/тестовая ветка. Canonical public product сейчас: amnezia_web-PRO.
GitHub: https://github.com/andrey271192/amnezia_web-PRO
Local: /Users/andrejbobyrev/andrey271192-repos/amnezia_web-PRO
Last pushed commit: c202f0b — chore: add npm lockfile
Правило
amnezia_web-PRO — основной публичный продукт.
amnezia_web-PRO_test — отдельный тестовый продукт, не смешивать с public PRO.
amnezia-web-pro-deploy — legacy/старый deploy repo, не использовать как основной продукт.
Install
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo bash
Defaults
Fresh login: admin
Fresh password: admin
Change password after first login.
AWG issue from client
Install-check видит два контейнера: amnezia-awg2 и amnezia-awg. Если веб показывает только awg2, причина в runtime-discovery/cache/UI-filter, не в Docker. Надо проверить конкретный VPS: docker ps, panel API/status, logs, затем исправить отображение обоих AWG profiles без ручного AWG_PROFILES.

View File

@@ -0,0 +1,22 @@
# 09.3.3 Fix AWG auto profiles 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Repo: https://github.com/andrey271192/amnezia_web-PRO
Commit: cbc20a1 — fix: auto-detect awg profiles on install
Что исправлено
Переустановка из GitHub работает для /opt/amnezia-admin без .git.
Установщик сохраняет текущий порт панели.
Установщик сохраняет текущий Docker volume /data.
Перед заменой /opt/amnezia-admin создаётся backup папки.
Если найден один контейнер amnezia-awg*, он становится AWG_CONTAINER.
Если найдено 2+ контейнера amnezia-awg*, AWG_PROFILES создаётся автоматически и сохраняется в /root/amnezia-admin.awg-profiles.json.
Dockerfile использует npm ci по package-lock.json.
Что сказать пользователю
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo env SKIP_LANDING=1 bash
После установки: обновить страницу Ctrl+F5. AWG контейнеры не удаляются и не пересоздаются.
Проверено
bash -n scripts/install.sh
bash -n scripts/uninstall.sh
node --check server.js
npm audit --audit-level=moderate

View File

@@ -0,0 +1,9 @@
# 09.3.4 Fix stale AWG profile snapshot 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Commit: 8ba2a26 — fix: refresh stale awg profile snapshots
Причина повтора бага: старый /root/amnezia-admin.awg-profiles.json мог содержать один профиль. Installer восстанавливал его и не пересобирал profiles по двум контейнерам.
Теперь если контейнеров amnezia-awg* два или больше, а восстановленный AWG_PROFILES содержит меньше двух container, installer автоматически пересобирает profiles и перезаписывает snapshot.
rm -f /root/amnezia-admin.awg-profiles.json
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s) | sudo env SKIP_LANDING=1 bash

View File

@@ -0,0 +1,24 @@
# 09.3.5 Client import from Amnezia backup 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Project: amnezia_web-PRO
Repo: https://github.com/andrey271192/amnezia_web-PRO
Commit: cff27e2 feat: import clients from amnezia configs
Что добавлено
В панели появился блок Импорт клиента.
Можно вставить готовый клиентский .conf или JSON backup Amnezia, если внутри есть текстовый .conf.
Если peer уже есть в awg0.conf, дубль не создаётся: восстанавливается/обновляется строка clientsTable.
Если peer отсутствует, он добавляется в текущий выбранный Инстанс.
Исходный .conf сохраняется в userData.last_config, чтобы дальше работали экспорт и управление из веба.
Добавлена защита: импортируемый .conf должен относиться к текущему серверу/инстансу по PublicKey сервера.
Команда переустановки для пользователя
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
Проверено
node --check server.js
node --check public/app.js
bash -n для всех scripts/*.sh
npm audit --audit-level=moderate: 0 vulnerabilities
git diff --check
Что сказать пользователю
Обновите панель командой выше, зайдите в веб, выберите нужный Инстанс и откройте блок Импорт клиента. Вставьте экспортированный клиентский .conf из приложения Amnezia или backup, где внутри есть этот .conf. После импорта клиент появится в таблице, а старые peer-дубли создаваться не будут.

View File

@@ -0,0 +1,22 @@
# 09.3.6 Fix duplicate AWG instances 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Project: amnezia_web-PRO
Commit: 8c67d7d fix: dedupe duplicate awg instances
Version: v1.2.32
Причина
После переустановки один и тот же AWG контейнер мог попасть в список дважды: из AWG_PROFILES и из saved managed instances. Старый dedup был только по id, поэтому при разных id веб показывал две одинаковые карточки.
Исправлено
Dedup профилей по container + confPath + iface + clientsPath.
/api/instances дополнительно дедупит runtime target после auto-resolve контейнера.
load/save managed profiles чистит дубли из instances.json.
Docs обновлены: раздел Инстансы задублировались.
Команда для пользователя
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
После установки: Ctrl+F5 в браузере.
Проверено
node --check server.js
node --check public/app.js
npm audit --audit-level=moderate: 0 vulnerabilities
git diff --check

View File

@@ -0,0 +1,15 @@
# 09.3.7 Fix AWG instance labels 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Commit: a9a67af fix: show distinct awg instance labels
Version: v1.2.33
Причина
У пользователя реально два контейнера: amnezia-awg2 и amnezia-awg. Порты и число клиентов разные, значит это не дубль. Но UI рисовал title по variant, поэтому обе карточки выглядели как AmneziaWG 2.0.
Исправлено
Карточка инстанса показывает profile label, а не только variant title.
Backend лучше определяет variant по profile/container/label до чтения конфига.
Docs уточнены: два разных контейнера с разными портами — нормальные два инстанса.
Команда
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
Потом Ctrl+F5.

View File

@@ -0,0 +1,18 @@
# 09.3.8 Fix clients selected profile 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Commit: cb5fca3 fix: load clients for selected profile
Version: v1.2.34
Симптом
Переключатель Инстанс показывает правильный контейнер, но таблица Пользователи показывает клиентов от другого/старого инстанса.
Причина
Создание/экспорт уже передавали profileId явно, но /api/clients и операции таблицы опирались на cookie выбранного профиля. Cookie мог не примениться/устареть, поэтому список клиентов приходил не от выбранного инстанса.
Исправлено
GET /api/clients принимает явный profileId.
disable/enable/rename/delete/disconnect-date используют runtimeFromExportRequest и body.profileId.
Frontend передаёт profileId во все запросы таблицы клиентов.
Docs обновлены.
Команда
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
Потом Ctrl+F5.

View File

@@ -0,0 +1,18 @@
# 09.3.9 Fix peers missing clientsTable 21.06
_Старая заметка из Notes (июнь 2026), перенесена 23.10.2026. Оригинал: Архив/Старые заметки Notes 20261023._
Commit: fd66b0a fix: show peers missing clients table
Version: v1.2.35
Симптом
Инстанс выбран правильно, но старые клиенты не появляются в таблице Пользователи.
Причина
UI строил список только из clientsTable. У старых/созданных приложением клиентов peer может быть в awg0.conf, а запись clientsTable отсутствовать или иметь другой формат.
Исправлено
GET /api/clients теперь возвращает union clientsTable + Peer из awg0.conf.
Peer-only строки показываются как Peer xxxx… и помечаются missingClientTable.
Rename/date/disable/delete умеют работать с peer-only строками.
Для экспорта .conf всё равно нужен импорт клиентского .conf, потому приватный ключ клиента сервер не знает.
Команда
curl -fsSL "https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh?nocache=$(date +%s)" | sudo env SKIP_LANDING=1 bash
Потом Ctrl+F5.

View File

@@ -1,15 +1,16 @@
FROM node:22-alpine
RUN apk add --no-cache docker-cli openssh-client sshpass
RUN apk add --no-cache docker-cli openssh-client sshpass bash iproute2 coreutils
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
COPY public ./public
ENV NODE_ENV=production

68
PROJECT_INDEX.md Normal file
View File

@@ -0,0 +1,68 @@
# PROJECT_INDEX — amnezia_web-PRO
Указатель проекта. Обновляется в конце каждой законченной работы вместе с
заметкой `09. История решений и грабли` и пушем в GitHub.
## Где что лежит
| Что | Где |
|---|---|
| Папка проекта | `/Volumes/ssd/Мои проекты/Открытые проекты/amnezia_web-PRO/` |
| Заметки | Apple Notes → `МОИ ПРОЕКТЫ` → папка `amnezia_web-PRO` |
| GitHub (публичный) | https://git.bobyrev.com/PCA/amnezia_web-PRO |
| Ветка прода | `main` |
| Старая закрытая панель (архив, не развивается) | `/Volumes/ssd/Мои проекты/Архив/amnezia_web-PRO_test/` |
Путь `/Volumes/andrey/Мои проекты/...` из старого промпта синхронизации **не существует** —
актуальная копия только на `/Volumes/ssd`.
## Состав репозитория
| Путь | Что это |
|---|---|
| `server.js` | Бэкенд панели (Node), порт контейнера 3980 |
| `public/` | Фронтенд админки |
| `landing/` | Публичная страница для приглашённых: `index.html`, `styles.css`, `nginx.conf`, `Dockerfile`, `admin-port.js` (порт админки для кнопки, пересобирается установщиком) |
| `Dockerfile` | Образ `amnezia-admin:latest` |
| `scripts/install.sh` | Установщик: сборка образов, запуск контейнеров, диагностика AmneziaWG |
| `scripts/uninstall.sh` | Снос панели и лендинга |
| `scripts/awg-instance.sh` | Работа с инстансами AmneziaWG |
| `scripts/clients-meta.mjs` | Снимок метаданных клиентов (имена, даты) поверх `clientsTable` |
| `scripts/test-clients-meta.mjs` | Тест к нему |
| `scripts/warp-amnezia.sh` | Обвязка WARP |
| `docs/panel-guide.md` | Руководство по панели |
| `README.md` | Порты, переменные окружения, установка, обновление |
Незакоммиченные разовые скрипты разбора в корне (`audit2.py`, `audit_conf.py`,
`check_conf.py`, `check_garbage.py`, `test_create.py`) — отладочные, в git не входят.
## Установка
```bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo bash
```
Первый вход: `admin` / `admin`, пароль сменить сразу.
## Порты и ключевые переменные
| Переменная | По умолчанию | Смысл |
|---|---|---|
| `HOST_PORT` | `8080` | Админ-панель |
| `LANDING_PORT` | `80` | Публичная страница |
| `SKIP_LANDING` | `0` | `1` — не поднимать лендинг |
| `LANDING_CONTAINER` | `amnezia-web-landing` | Имя контейнера лендинга |
| `CONTAINER_NAME` | `amnezia-admin` | Имя контейнера панели |
## Грабли, которые стоят времени
- **Контейнеры переживают переустановку.** Всё поднимается с `--restart unless-stopped`.
`SKIP_LANDING=1` на повторной установке не отключал лендинг, пока установщик не начал
сносить старый контейнер в ветке пропуска (`7b92bfb`, 08.09.2026). Проверка:
`docker ps --filter name=amnezia-web-landing`.
- **Коммиты сами в контейнер не попадают** — нужна пересборка образа, см. README.
- Разбор остальных случаев — в заметке `09. История решений и грабли`.
## Состояние
- v1.3.0 (23.10.2026), тег и релиз на GitHub. Gitea — допушить (была недоступна).

243
README.md
View File

@@ -1,22 +1,101 @@
# amnezia_web-PRO
Веб-панель на вашем VPS для управления клиентами **AmneziaWG**: вкл/выкл, удаление, переименование, дата отключения; при нескольких контейнерах — переключатель **«Инстанс»** (`AWG_PROFILES`); **экспорт .conf** при наличии `last_config`; **новый клиент под каскад** (свой Endpoint и ключи на сервере). **Cloudflare WARP** — *необязательное* дополнение: часть клиентов может выходить в интернет через интерфейс `warp` внутри контейнера AWG (ставится отдельно скриптом на хосте). Работает через Docker и `docker exec` в контейнер **Amnezia** (по умолчанию `amnezia-awg2`).
Полностью открытая веб-панель для сервера Amnezia: все функции бесплатны, без платных или закрытых редакций. Один репозиторий, один установщик.
Справочник по интерфейсу, типичным сбоям и HTTP API: **[docs/panel-guide.md](docs/panel-guide.md)**.
<img src="docs/screenshots/panel-1.4.png" alt="Панель amnezia_web-PRO 1.4: пользователи AmneziaWG, Xray VLESS-Reality, протоколы и инстансы" width="780"/>
**Безопасность:** контейнер с монтированием `docker.sock` эквивалентен root на хосте — используйте сложный пароль и по возможности ограничьте доступ по IP или TLS.
**Что умеет:** клиенты AmneziaWG (создать, выключить, дата отключения, .conf), установка протоколов из браузера (AmneziaWG 1.x/2.0/3.0/3.1, WireGuard), Xray VLESS‑Reality, Cloudflare WARP и каскад через другой сервер, Telegram MTProto‑прокси, домен сервера в конфигах, светлая/тёмная тема.
### Редакции: открытая база и PRO
**Установка** (Ubuntu 22.04/24.04, Debian 12, под root; Docker поставится сам):
- **[amnezia_web](https://github.com/andrey271192/amnezia_web)** — открытый репозиторий **базовой** панели: **только просмотр** клиентов AmneziaWG и статусов. Нет включения/выключения peer в туннеле, правки дат отключения, переименования, удаления, экспорта `.conf`, блока «Новый клиент под каскад», Cloudflare WARP и синхронизации времени хоста по SSH. Редакция задаётся файлом **`.amnezia-panel-edition`** со значением `community` (его подставляет установщик этого форка) или переменной **`AMNEZIA_EDITION=community`**. Текст и кнопка «Разблокировать PRO» используют **`COMMUNITY_UPGRADE_URL`** и **`COMMUNITY_UPGRADE_PITCH`** (по умолчанию URL ведёт на страницу подписки уровня PRO на Boosty); полный код и приватный репозиторий **amnezia_web-PRO** подключаются подписчикам отдельно.
```bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo bash
```
- **Этот репозиторий (`amnezia_web-PRO`)** — **полная** панель (редакция **pro** по умолчанию): все функции из README ниже активны, если не задана **`AMNEZIA_EDITION=community`**.
Панель: `http://IP:8080`, вход — пароль `admin` (смените сразу). Код — [git.bobyrev.com/PCA/amnezia_web-PRO](https://git.bobyrev.com/PCA/amnezia_web-PRO) (GitHub-аккаунт заблокирован, старые ссылки raw.githubusercontent.com больше не работают).
## Что нового в 1.4.2
- **Проект переехал на [git.bobyrev.com/PCA/amnezia_web-PRO](https://git.bobyrev.com/PCA/amnezia_web-PRO)** — GitHub-аккаунт заблокирован. Установщик и обновление качают код оттуда. Старая команда с `raw.githubusercontent.com` больше не работает — используйте новую (выше).
## Что нового в 1.4.0
- **Xray VLESS‑Reality** — раздел «Xray VLESS‑Reality»: установка с выбором TCP‑порта и сайта‑маскировки, клиенты (создать, выключить/включить, удалить), ссылка `vless://` для Amnezia VPN, v2rayNG, Hiddify, Streisand, Happ. Файлы в `/opt/amnezia/xray` в том же формате, что у приложения Amnezia.
- **Установщик сам ставит Docker** на чистом сервере.
- **Всё открыто:** убрана редакция «community / только просмотр» и переменные `AMNEZIA_EDITION`, `COMMUNITY_UPGRADE_*` — все функции доступны всем.
## Что нового в 1.3.0
- **Установка протоколов прямо с сайта** (раздел «Протоколы / инстансы»), несколько версий рядом на разных портах:
| Вариант | Для кого |
|---|---|
| AmneziaWG 3.1 | новейший: защита заголовков, случайные хвосты; свежее приложение Amnezia |
| AmneziaWG 3.0 | HeaderProtectionKey, паддинг |
| AmneziaWG 2.0 | рекомендуемый по умолчанию (S3/S4) |
| AmneziaWG 1.x | роутеры Keenetic/OpenWrt и старые приложения (без S3/S4) |
| WireGuard (Legacy) | любой WG-клиент, без обфускации |
- **Исправлено:** конфиг клиента для AWG 1.x получал лишние S3/S4/I1 — рукопожатие было, интернета нет. Теперь в .conf попадают только параметры, которые есть на сервере.
- **Исправлено:** при обновлении установщик записывал контейнеры панели в `AWG_PROFILES`, и переключение инстанса создавало клиента не там.
- **Исправлено:** проверка занятого UDP-порта смотрела внутрь контейнера панели и ничего не находила.
- **Домен сервера** — поле «Домен или IP сервера в конфигах клиентов»: подставляется в Endpoint всех выгрузок, домен проверяется на резолв.
- **Настоящий каскад** — в разделе «Cloudflare WARP / каскад» вставьте клиентский .conf другого сервера: отмеченные клиенты выходят в интернет через него (переживает перезапуск контейнера).
- **Светлая / тёмная тема** — кнопка «◐ Тема» в шапке (по умолчанию как в системе).
- Старые установки не затрагиваются: контейнеры, поставленные приложением Amnezia, остаются как были.
## Быстрый старт: от пустого сервера до VPN за 10 минут
**Что нужно:** VPS с Ubuntu 22.04/24.04 или Debian 12 (1 ГБ ОЗУ хватает), доступ root по SSH. Docker ставить не нужно — установщик поставит его сам.
**1. Установите панель.** Зайдите на сервер по SSH и выполните:
```bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo bash
```
В конце будет строка `Админ-панель: http://IP:8080`.
**2. Откройте порты** в фаерволе хостинга (панель управления VPS) и, если включён, в `ufw`:
| Порт | Зачем |
|---|---|
| `8080/tcp` | админ-панель |
| `80/tcp` | страница для пользователей (необязательно, `SKIP_LANDING=1` — без неё) |
| UDP-порт AmneziaWG, который выберете на шаге 4 (например `41010/udp`) | VPN AmneziaWG |
| TCP-порт Xray, если будете ставить (например `443/tcp`) | VPN Xray |
```bash
sudo ufw allow 8080/tcp && sudo ufw allow 41010/udp # пример, если ufw включён
```
**3. Войдите** на `http://IP:8080` — пароль **`admin`**. Сразу смените его в блоке «Сменить пароль».
**4. Разверните протокол.** Раздел «Протоколы / инстансы» → выберите вариант → порт (например `41010`) → «Развернуть инстанс». Какой выбрать:
- **AmneziaWG 2.0** — по умолчанию, для телефонов и ПК (приложение Amnezia VPN);
- **AmneziaWG 3.1 / 3.0** — самые новые, нужна свежая версия приложения;
- **AmneziaWG 1.x** — для роутеров Keenetic / OpenWrt;
- **WireGuard** — для любых WG-клиентов, без маскировки.
Если на сервере уже стоит VPN, поставленный приложением Amnezia, — шаг пропустите: панель найдёт его сама.
**5. Создайте клиента.** Блок «Новый клиент» → имя → «Создать и скачать .conf». Файл импортируйте в Amnezia VPN (на телефоне/ПК: «+» → «Файл с настройками»).
**6. (по желанию) Xray VLESS‑Reality** — если AmneziaWG у провайдера режут. Раздел «Xray VLESS‑Reality» → порт TCP (`443`, если свободен) и сайт‑маскировка → «Установить Xray» (~1 минута) → «Создать» клиента — ссылка `vless://` скопируется. Вставьте её в Amnezia VPN, v2rayNG, Hiddify, Streisand или Happ («+» → «Из буфера обмена»).
**7. Свой домен (по желанию).** В «Протоколы / инстансы» → «Домен или IP сервера в конфигах клиентов» → «Сохранить адрес». Без него в конфиги подставляется IP.
**Обновление** — та же команда из шага 1: клиенты, пароль и настройки сохраняются. **Удаление** — см. «Удаление одной командой».
**Если не работает:**
- «Рукопожатие есть, интернета нет» или нет рукопожатия — не открыт UDP-порт у хостинга (шаг 2).
- Панель не открывается — не открыт `8080/tcp`; проверка на сервере: `curl -s localhost:8080/health`.
- Xray: «TCP-порт занят» — выберите другой порт (например `8443`) и откройте его.
- Логи панели: `sudo docker logs --tail 50 amnezia-admin`.
## Что открывается по какому порту
| Адрес | Назначение |
|-------|------------|
| **`http://IP:HOST_PORT`** (по умолчанию **`:8080`**) | **Админ-панель:** клиенты AmneziaWG, опциональный блок **Cloudflare WARP**, каскад, экспорт `.conf`, смена пароля. Внизу страницы — футер со ссылками автора (GitHub, донат, Telegram и т.д.). |
| **`http://IP:HOST_PORT`** (по умолчанию **`:8080`**) | **Админ-панель:** клиенты AmneziaWG, опциональные блоки **Telegram MTProto‑прокси** и **Cloudflare WARP**, каскад, экспорт `.conf`, смена пароля. Внизу страницы — футер со ссылками автора (GitHub, донат, Telegram и т.д.). |
| **`http://IP:LANDING_PORT`** (по умолчанию **`:80`**) | **Публичная страница** для приглашённых: инструкция, кнопка перехода в админку (`landing/admin-port.js` подставляет тот же `HOST_PORT`). Текст «проблемы — администратору сервера» и дисклеймер. **Без** блока доната и личных ссылок автора — они только в админке. |
Если порт **80** занят другим сервисом, задайте **`LANDING_PORT`** (например `8081`) или **`SKIP_LANDING=1`**. На работу панели по **`HOST_PORT`** это не влияет.
@@ -29,16 +108,32 @@
<img src="docs/screenshots/panel-overview-password.png" alt="Панель: заголовок, подсказки по расписанию и нескольким инстансам, форма смены пароля" width="780"/>
</p>
Файлы: `docs/screenshots/panel-users-table.png`, `docs/screenshots/panel-overview-password.png` — при желании замените их своими скриншотами с теми же именами.
Файлы: `docs/screenshots/panel-users-table.png`, `docs/screenshots/panel-overview-password.png` — замените на свои живые PNG при необходимости.
---
## Установка одной командой
## Установка
На сервере под **root** (или через `sudo`), когда репозиторий уже опубликован на GitHub:
### Public install from GitHub
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo bash
```
Панель после установки открывается по **`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://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo bash
```
Если не нужен лендинг на порту 80:
```bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo env SKIP_LANDING=1 bash
```
### Переменные окружения и `sudo`
@@ -50,7 +145,7 @@ curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/s
1. **`sudo -E bash`** вместо `sudo bash` (для большинства VPS достаточно):
```bash
AWG_PROFILES='[{"id":"awg",…}]' curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo -E bash
AWG_PROFILES='[{"id":"awg",…}]' curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo -E bash
```
2. **Файл на VPS** (способ без `-E`): записать JSON **одной строкой** в **`/root/amnezia-admin.awg-profiles.json`** (например `umask 077; printf '%s\n' '[{"id":"awg",…}]' > /root/amnezia-admin.awg-profiles.json`), затем обычный **`curl … | sudo bash`** — установщик подставит профили из файла (см. абзац про снимок ниже таблицы).
@@ -78,13 +173,13 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas
| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `GITHUB_REPO` | `andrey271192/amnezia_web-PRO` | Откуда качать архив |
| `GITHUB_REPO` | `PCA/amnezia_web-PRO` | Откуда качать архив |
| `BRANCH` | `main` | Ветка |
| `INSTALL_DIR` | `/opt/amnezia-admin` | Куда распаковать исходники |
| `DATA_DIR` | `/opt/amnezia-admin-data` | Том с `password.hash` и сессией |
| `HOST_PORT` | `8080` | Порт HTTP панели на хосте |
| `AWG_CONTAINER` | `amnezia-awg2` | Имя контейнера Amnezia WG |
| `AWG_PROFILES` | _(нет)_ | JSON-массив профилей: несколько контейнеров/путей (см. ниже). Если задан — переключатель «Инстанс» в вебе. При однострочной установке с переменной в окружении нужен **`sudo -E bash`** или файл **`/root/amnezia-admin.awg-profiles.json`** — см. раздел **«Переменные окружения и sudo»** выше. В каждом объекте можно задать `warpDir`, `warpConf`, `warpClientsList`, `startScript` — см. раздел **Cloudflare WARP** |
| `AWG_CONTAINER` | `amnezia-awg` | Имя контейнера Amnezia WG |
| `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` |
@@ -100,41 +195,73 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas
| `EXPORT_CONFIG_SECRET` | _(нет)_ | Секрет для прямой ссылки без входа в панель: `GET /api/clients/export-config?token=СЕКРЕТ&clientId=…&profileId=…` (при нескольких инстансах `profileId` обязателен). Не светите URL посторонним |
| `ADMIN_PASSWORD` | _(генерируется)_ | Первый пароль вместо файла |
| `SKIP_DOWNLOAD` | `0` | `1` — не качать GitHub, собрать из `INSTALL_DIR` |
| `ALLOW_DEFAULT_PASSWORD` | `0` | `1` — см. раздел «Пароль» ниже |
| `SKIP_LANDING` | `0` | `1` — не поднимать nginx-лендинг на `LANDING_PORT` |
| `LANDING_PORT` | `80` | Порт nginx-лендинга (если `80` занят — например `8081`) |
| `LANDING_CONTAINER` | `amnezia-web-landing` | Имя контейнера лендинга |
| `NO_CACHE` | `0` | `1` — `docker build --no-cache` при проблемах с обновлением образа |
| `UI_HIDE_SECTIONS` | _(нет)_ | Список через запятую: **`users`**, **`warp`**, **`cascade`** — скрыть блоки в веб-панели (см. ниже). При обновлении через `install.sh` значение подтягивается из старого контейнера, если не задано заново |
| `UI_HIDE_SECTIONS` | _(нет)_ | Список через запятую: **`users`**, **`warp`**, **`cascade`**, **`mtproto`** — скрыть блоки в веб-панели (см. ниже). При обновлении через `install.sh` значение подтягивается из старого контейнера, если не задано заново |
| `UI_HIDE_USERS` | `0` | `1` или `true` — эквивалент `users` в `UI_HIDE_SECTIONS` |
| `UI_HIDE_WARP` | `0` | `1` — эквивалент `warp` (также блокируются `POST /api/warp/*`) |
| `UI_HIDE_CASCADE` | `0` | `1` — эквивалент `cascade` (блокируется `POST /api/clients/create-cascade`) |
| `UI_HIDE_MTPROTO` | `0` | `1` или **`mtproto`** в `UI_HIDE_SECTIONS` — скрыть блок установки MTProto; **`GET/POST /api/mtproto/*`** отвечают **403** |
| `MTPRO_PROXY_CONTAINER` | `mtproto-proxy` | Имя контейнера MTProto (образ **telegrammessenger/proxy**) |
| `MTPRO_PROXY_IMAGE` | `telegrammessenger/proxy:latest` | Образ **`docker pull`** при установке из панели |
| `MTPRO_INTERNAL_PORT` | `443` | Порт процесса **внутри** контейнера прокси |
| `MTPRO_PUBLISH_PORT` | `8443` | Порт **хоста**, проброшенный наружу (`-p`) |
| `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 и т.д.)
Пути и имена контейнеров на сервере могут отличаться — проверьте внутри контейнера (`docker exec … ls /opt/amnezia`). Установщик сохраняет JSON в **`/root/amnezia-admin.awg-profiles.json`** и при следующем запуске подставляет его, если вы не передали `AWG_PROFILES`.
**Пример 1** — классическая схема: новый AWG в `amnezia-awg2`, Legacy в отдельном контейнере с каталогом `wireguard`:
**Пример 1** — классическая схема: основной AWG в `amnezia-awg`, Legacy в отдельном контейнере с каталогом `wireguard`:
```bash
AWG_PROFILES='[{"id":"awg","label":"AmneziaWG","container":"amnezia-awg2","confPath":"/opt/amnezia/awg/awg0.conf","clientsPath":"/opt/amnezia/awg/clientsTable","iface":"awg0","wgBinary":"awg","pskPath":"/opt/amnezia/awg/wireguard_psk.key"},{"id":"legacy","label":"AmneziaWG Legacy","container":"amnezia-wg0","confPath":"/opt/amnezia/wireguard/wg0.conf","clientsPath":"/opt/amnezia/wireguard/clientsTable","iface":"wg0","wgBinary":"wg","pskPath":"/opt/amnezia/wireguard/wireguard_psk.key"}]' \
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo -E bash
AWG_PROFILES='[{"id":"awg","label":"AmneziaWG","container":"amnezia-awg","confPath":"/opt/amnezia/awg/awg0.conf","clientsPath":"/opt/amnezia/awg/clientsTable","iface":"awg0","wgBinary":"awg","pskPath":"/opt/amnezia/awg/wireguard_psk.key"},{"id":"legacy","label":"AmneziaWG Legacy","container":"amnezia-wg0","confPath":"/opt/amnezia/wireguard/wg0.conf","clientsPath":"/opt/amnezia/wireguard/clientsTable","iface":"wg0","wgBinary":"wg","pskPath":"/opt/amnezia/wireguard/wireguard_psk.key"}]' \
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo -E bash
```
**Пример 2** — оба конфигурационных набора внутри контейнера **`amnezia-awg`** (файл `wg0.conf` рядом с `awg0.conf` в `/opt/amnezia/awg/`), второй контейнер **`amnezia-awg2`**:
**Пример 2** — оба конфигурационных набора внутри контейнера **`amnezia-awg`** (файл `wg0.conf` рядом с `awg0.conf` в `/opt/amnezia/awg/`):
```bash
AWG_PROFILES='[{"id":"awg","label":"AmneziaWG","container":"amnezia-awg2","confPath":"/opt/amnezia/awg/awg0.conf","clientsPath":"/opt/amnezia/awg/clientsTable","iface":"awg0","wgBinary":"awg","pskPath":"/opt/amnezia/awg/wireguard_psk.key"},{"id":"legacy","label":"AmneziaWG Legacy","container":"amnezia-awg","confPath":"/opt/amnezia/awg/wg0.conf","clientsPath":"/opt/amnezia/awg/clientsTable","iface":"wg0","wgBinary":"wg","pskPath":"/opt/amnezia/awg/wireguard_psk.key"}]' \
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo -E bash
AWG_PROFILES='[{"id":"awg","label":"AmneziaWG","container":"amnezia-awg","confPath":"/opt/amnezia/awg/awg0.conf","clientsPath":"/opt/amnezia/awg/clientsTable","iface":"awg0","wgBinary":"awg","pskPath":"/opt/amnezia/awg/wireguard_psk.key"},{"id":"legacy","label":"AmneziaWG Legacy","container":"amnezia-awg","confPath":"/opt/amnezia/awg/wg0.conf","clientsPath":"/opt/amnezia/awg/clientsTable","iface":"wg0","wgBinary":"wg","pskPath":"/opt/amnezia/awg/wireguard_psk.key"}]' \
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo -E bash
```
Для **Legacy** часто используется обычный `wg`, для новой AmneziaWG — **`awg`**; поле **`pskPath`** желательно указывать явно, если путь к `wireguard_psk.key` нестандартный.
Если в вебе пропал список **«Инстанс»**, смотрите раздел «Если пропал список Инстанс» в **[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 нужен только при нестандартных путях внутри контейнеров.
### Импорт клиента из Amnezia backup / `.conf`
В панели есть блок **«Импорт клиента»**. Он нужен, когда клиент уже создан в приложении Amnezia, но в веб-панели его нет или у строки нет `last_config`.
Что поддерживается:
- вставка готового клиентского **`.conf`** с секциями `[Interface]` и `[Peer]`;
- вставка JSON backup, если внутри него есть строка с таким `.conf`;
- восстановление записи в **`clientsTable`** без дублирования peer, если peer уже есть в `awg0.conf`;
- добавление peer в текущий инстанс, если его ещё нет на сервере;
- сохранение исходного `.conf` в `userData.last_config`, чтобы дальше работали экспорт и управление из веба.
Перед импортом выберите нужный **«Инстанс»** вверху панели. Если backup приложения зашифрован или не содержит текстовый `.conf`, сначала экспортируйте клиентский конфиг из приложения Amnezia и вставьте его в форму импорта.
### Telegram MTProto‑прокси
**Не связано с AmneziaWG.** Отдельный контейнер образа **[telegrammessenger/proxy](https://hub.docker.com/r/telegrammessenger/proxy)** на том же хосте. После входа в панель: установка, перезапуск, удаление, выбор порта на хосте, просмотр логов и ссылки **`tg://proxy`** (хост из **`MTPRO_PUBLIC_HOST`**, **`CLIENT_CONFIG_ENDPOINT`** или из URL страницы панели).
- **`GET /api/mtproto/status`** — состояние и ссылка; **`?withLogs=1`** добавляет хвост логов (**`logsFetched`**).
- **`GET /api/mtproto/logs`** и **`GET /api/mtproto/tail`** — хвост **`docker logs`** (отдельный путь **`/tail`**, если прокси обрезает путь **`…/logs`**).
- **`POST /api/mtproto/install`** | **`…/restart`** | **`…/remove`**
За nginx/Caddy нужно проксировать **весь** префикс **`/api/`** на процесс панели. После сборки образа можно свериться: **`GET /health`** отдаёт **`version`** из `package.json`.
Чтобы **скрыть** блок и отключить API: **`UI_HIDE_MTPROTO=1`** или токен **`mtproto`** в **`UI_HIDE_SECTIONS`**.
### Cloudflare WARP (необязательно)
@@ -183,20 +310,28 @@ cd /opt/amnezia-admin
| `users` | Секция **«Пользователи»** (таблица) и отладочный вывод **awg show**. Запрос **`GET /api/clients`** по-прежнему нужен странице — API для вкл/выкл peer и экспорта **не отключается**, скрыта только таблица. |
| `warp` | Блок **Cloudflare WARP**; запросы **`POST /api/warp/*`** отвечают **403**. |
| `cascade` | Блок **«Новый клиент под каскад»**; **`POST /api/clients/create-cascade`** отвечает **403**. |
| `mtproto` | Блок **«Telegram MTProto‑прокси»**; **`GET`/`POST /api/mtproto/*`** отвечают **403**. |
Пример установки / обновления с частично пустой панелью:
```bash
UI_HIDE_SECTIONS=warp,cascade curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo -E bash
UI_HIDE_SECTIONS=warp,cascade curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo -E bash
```
Отдельные флаги **`UI_HIDE_USERS`**, **`UI_HIDE_WARP`**, **`UI_HIDE_CASCADE`** (`1` или `true`) дублируют соответствующий токен.
Отдельные флаги **`UI_HIDE_USERS`**, **`UI_HIDE_WARP`**, **`UI_HIDE_CASCADE`**, **`UI_HIDE_MTPROTO`** (`1` или `true`) дублируют соответствующий токен.
Итоговые URL после установки совпадают с таблицей **«Что открывается по какому порту»** в начале этого файла; установщик записывает выбранный `HOST_PORT` в **`landing/admin-port.js`**, чтобы кнопка на лендинге вела на нужную админку.
### Каскад (свой Endpoint для клиента)
В панели блок **«Новый клиент под каскад»**: задаёте IP/DNS и при необходимости порт — сервер **генерирует ключи**, добавляет peer в текущий инстанс AmneziaWG и отдаёт `.conf`, где **Endpoint** указывает на ваш промежуточный узел. На этом узле нужен **проброс UDP** на VPS (тот же порт, что слушает WG на сервере, либо свой порт из формы).
Сценарий: **устройство в интернете подключается не к вашему «основному» VPS напрямую**, а к **промежуточному узлу** (ещё один VPS, домашний роутер с пробросом и т.д.), а уже этот узел **пересылает UDP** на порт WireGuard вашего основного VPS, где живёт AmneziaWG и эта панель.
Что нужно человеку, который ставит связку:
1. На **этом** (основном) VPS — обычный AmneziaWG и эта панель; здесь блок **«Новый клиент под каскад»** **создаёт peer и ключи** и выдаёт скачанный **`.conf`**, в котором **Endpoint** уже указывает на промежуточную машину.
2. На **промежуточном** сервере нужен **проброс порта DNAT до основного VPS** на тот же **ListenPort**, что слушает туннель здесь (либо свой внешний порт → тогда его указывают в форме как «Порт Endpoint»).
Готовая панель для правил проброса (iptables DNAT NAT «под ключ»): репозиторий **[andrey271192/kaskad_web_vpn](https://github.com/andrey271192/kaskad_web_vpn)** — установка `curl … install.sh | sudo bash`, затем вход в браузере на промежуточный VPS по порту из README (например `8088/setup`) и настройка правила «вход → IP основного VPS:порт WG». Подробные шаги и переменные среды см. там же в README.
### Экспорт конфигурации клиента (.conf)
@@ -216,16 +351,16 @@ UI_HIDE_SECTIONS=warp,cascade curl -fsSL https://raw.githubusercontent.com/andre
Коммиты на GitHub **сами не попадают** в уже запущенный контейнер: нужно заново **скачать код и пересобрать образ** (`amnezia-admin` и, если не отключали лендинг, **`amnezia-web-landing`**).
Проще всего — та же команда, что при установке (данные в `DATA_DIR`, пароль и `AWG_PROFILES` сохранятся — профили подтягиваются из файла на VPS или из старого контейнера). Скрипт **подставит прежний внешний порт** контейнера `amnezia-admin`, если вы не передавали `HOST_PORT` и в коде по умолчанию стоит `8080`:
Проще всего для пути из исходников — та же команда, что при установке (данные в `DATA_DIR`, пароль и `AWG_PROFILES` сохранятся — профили подтягиваются из файла на VPS или из старого контейнера). Скрипт **подставит прежний внешний порт** контейнера `amnezia-admin`, если вы не передавали `HOST_PORT` и в коде по умолчанию стоит `8080`:
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo bash
```
Если интерфейс после этого всё ещё старый — принудительно без кэша слоёв Docker:
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo NO_CACHE=1 bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo NO_CACHE=1 bash
```
Если проект уже лежит в `/opt/amnezia-admin` через git и вы подтянули изменения (`git pull`), пересоберите без повторной загрузки архива:
@@ -251,54 +386,41 @@ sudo docker run --rm amnezia-admin:latest grep -Eo 'Новый клиент по
## Удаление одной командой
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/uninstall.sh | sudo bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/uninstall.sh | sudo bash
```
Полная очистка (контейнер, образ, данные панели и каталог `/opt/amnezia-admin`):
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/uninstall.sh | sudo REMOVE_IMAGE=1 REMOVE_DATA=1 REMOVE_SRC=1 bash
curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/uninstall.sh | sudo REMOVE_IMAGE=1 REMOVE_DATA=1 REMOVE_SRC=1 bash
```
---
## Первый вход и пароль
### Режим по умолчанию (рекомендуется)
### Режим по умолчанию
Скрипт установки **генерирует** пароль и записывает его в файл на сервере:
На пустой установке создаётся первый вход:
```bash
sudo cat /root/amnezia-admin.initial-password
```
- логин: **`admin`**
- пароль: **`admin`**
Войдите в панель этим паролем и сразу смените его в блоке **«Сменить пароль»**.
Смените пароль сразу после входа в блоке **«Сменить пароль»**.
### Свой пароль при установке
```bash
ADMIN_PASSWORD='ВашНадёжныйПароль' curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo -E bash
ADMIN_PASSWORD='ВашНадёжныйПароль' curl -fsSL https://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/main/scripts/install.sh | sudo -E bash
```
После первого успешного старта переменную `ADMIN_PASSWORD` из команды `docker run` убирайте — пароль уже в томе `DATA_DIR`.
### Пароль по умолчанию из документации (только тест / лаборатория)
Если задать при установке **`ALLOW_DEFAULT_PASSWORD=1`**, контейнер создаёт пароль из README:
- **Логин в веб:** пароль по умолчанию **`AmneziaAdmin!ChangeMe`**
Его можно переопределить переменной **`DEFAULT_ADMIN_PASSWORD`** в окружении контейнера до первого создания `password.hash`.
На продакшене этот режим **не рекомендуется**.
### Ручной Docker без скрипта
Нужны том `-v /путь/данных:/data` и **один из** вариантов:
Нужен том `-v /путь/данных:/data`.
1. `-e ADMIN_PASSWORD=...` при **первом** запуске (файла `password.hash` ещё нет);
2. `-e ALLOW_DEFAULT_PASSWORD=1` — см. пароль выше;
3. готовый файл `password.hash` в томе (продвинутый сценарий).
Если `password.hash` ещё нет, контейнер создаст `admin/admin`. Для своего первого пароля передайте `-e ADMIN_PASSWORD=...`. Для продвинутого сценария можно заранее положить готовый `password.hash` в том.
---
@@ -321,7 +443,7 @@ ADMIN_PASSWORD=localtest node server.js
| Способ | Ссылка |
|--------|--------|
| GitHub | [amnezia_web-PRO](https://github.com/andrey271192/amnezia_web-PRO) |
| GitHub | [amnezia_web-PRO](https://git.bobyrev.com/PCA/amnezia_web-PRO) |
| Boosty | [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate) |
| Ozon СБП | [Ozon СБП](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798) |
| Telegram | [@lot_andrey](https://t.me/lot_andrey) |
@@ -337,3 +459,14 @@ ADMIN_PASSWORD=localtest node server.js
## Лицензия
MIT, см. [LICENSE](LICENSE).
---
## Поддержка проекта
---
- ⭐ **GitHub:** [PCA/amnezia_web-PRO](https://git.bobyrev.com/PCA/amnezia_web-PRO)
- 💖 **Boosty:** [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate)
- 💳 **Ozon Bank (СБП):** [ссылка](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
- ✉️ **Telegram:** [@lot_andrey](https://t.me/lot_andrey)

View File

@@ -6,10 +6,12 @@
| Блок в интерфейсе | Назначение |
|-------------------|------------|
| **Инстанс** | Переключение между профилями из `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). |
| **Новый клиент под каскад** | Создание нового peer на сервере с **вашим Endpoint** (промежуточный узел); выдача готового `.conf`. |
| **Telegram MTProto‑прокси** | *Необязательно*, не часть AWG. Docker-контейнер официального образа **telegrammessenger/proxy**; установка из панели, ссылка **`tg://proxy`**, переменные **`MTPRO_*`** — см. основной [README](../README.md). |
| **Новый клиент под каскад** | Клиент в конфиге смотрит на **промежуточный узел** по Endpoint; peer и ключи создаются **на текущем VPS** и отдаются в `.conf`. На промежуточном сервере заранее нужен проброс порта DNAT UDP до WG этого VPS ([**kaskad_web_vpn**](https://github.com/andrey271192/kaskad_web_vpn) или свой проброс); в блоке см. текст и команду установки каскад-панели. |
| **Импорт клиента** | Перенос готового клиента из `.conf` или JSON backup Amnezia, если backup содержит текстовый конфиг. Панель восстанавливает строку в `clientsTable`, сохраняет `last_config` и добавляет peer только если его ещё нет в серверном конфиге. |
| **Пользователи** | Вкл/выкл peer, удаление, переименование, даты отключения; экспорт `.conf` если в записи есть `userData.last_config`. |
## Переключатель «Инстанс» не отображается
@@ -18,8 +20,31 @@
```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://git.bobyrev.com/PCA/amnezia_web-PRO/raw/branch/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).
## Инстансы задублировались
После переустановки старый `AWG_PROFILES` и сохранённые managed-инстансы могли указывать на один и тот же контейнер/конфиг с разными `id`. Начиная с **v1.2.32** панель дедуплицирует профили по `container + confPath + iface + clientsPath`, а также чистит дубли при чтении/сохранении `instances.json`. Обновите панель из GitHub и сделайте жёсткое обновление страницы.
Если проверка на сервере показывает два разных контейнера, например `amnezia-awg2` и `amnezia-awg`, с разными портами и разным числом клиентов — это не дубль, а два инстанса. Начиная с **v1.2.33** карточки используют реальное имя профиля (`label`), чтобы `AmneziaWG 2.0` и `AmneziaWG` не выглядели одинаково.
Если переключатель **«Инстанс»** показывает правильный контейнер, но таблица **«Пользователи»** остаётся от другого инстанса, обновите до **v1.2.34**. Клиентские запросы (`/api/clients` и операции над пользователями) теперь всегда передают явный `profileId`, а не полагаются только на cookie выбранного профиля.
Если клиенты есть в `awg0.conf`, но не отображаются из-за отсутствия/нестандартного `clientsTable`, обновите до **v1.2.35**. Таблица **«Пользователи»** теперь показывает объединение `clientsTable + [Peer]` из серверного конфига. Такие строки помечаются подсказкой: для экспорта `.conf` нужно импортировать клиентский `.conf` через блок **«Импорт клиента»**.
Начиная с **v1.2.36** в блоке **«Пользователи»** есть кнопка **«Подтянуть из awg0.conf»**. Она создаёт недостающие строки `clientsTable` для уже активных `[Peer]`, которые были созданы в приложении Amnezia. VPN-конфиг и контейнер не пересоздаются. Экспорт `.conf` для таких пользователей появится только после импорта их клиентского `.conf`, потому приватный ключ клиента хранится в приложении, а не на сервере.
Начиная с **v1.2.37** парсер `clientsTable` поддерживает не только массив, но и object/map/nested-форматы разных установок Amnezia. Для диагностики источников есть `GET /api/clients/source-report?profileId=...`: показывает текущий контейнер, пути, количество `[Peer]`, количество строк `clientsTable`, missing/table-only счётчики и короткие префиксы ключей без публикации полных ключей.
Начиная с **v1.2.38** панель сама выбирает рабочие пути внутри контейнера: среди `/opt/amnezia/awg/awg0.conf`, `/opt/amnezia/awg/wg0.conf`, `/opt/amnezia/wireguard/wg0.conf` берётся конфиг с максимальным числом `[Peer]`; среди `/opt/amnezia/awg/clientsTable` и `/opt/amnezia/wireguard/clientsTable` берётся таблица с максимальным числом клиентов. Это закрывает установки, где приложение Amnezia создало старых клиентов в `wg0.conf`, а не в `awg0.conf`.
Начиная с **v1.2.39** панель auto-detect делает не только для путей, но и для binary внутри контейнера. Если профиль указывает `wgBinary=awg`, а в контейнере есть только `wg`, команды `show`, `pubkey`, `genkey`, `syncconf` используют найденный binary. Это закрывает ошибку `sh: awg: not found` на контейнерах вида `amneziawg`.
## Лендинг не поднимается (порт 80 занят)
@@ -44,16 +69,18 @@
## Скрытие разделов в панели (`UI_HIDE_*`)
Переменные контейнера **`amnezia-admin`**: **`UI_HIDE_SECTIONS`** (список `users`, `warp`, `cascade`) или отдельно **`UI_HIDE_USERS`**, **`UI_HIDE_WARP`**, **`UI_HIDE_CASCADE`** (`1` / `true`). Подробности и пример — в таблице установки и разделе README про **`UI_HIDE_*`**.
Переменные контейнера **`amnezia-admin`**: **`UI_HIDE_SECTIONS`** (список `users`, `warp`, `cascade`, **`mtproto`**) или отдельно **`UI_HIDE_USERS`**, **`UI_HIDE_WARP`**, **`UI_HIDE_CASCADE`**, **`UI_HIDE_MTPROTO`** (`1` / `true`). Подробности и пример — в таблице установки и разделе README про **`UI_HIDE_*`**.
- **`warp`** — скрывает блок WARP; **`POST /api/warp/*`** → 403.
- **`cascade`** — скрывает каскад; **`POST /api/clients/create-cascade`** → 403.
- **`users`** — скрывает таблицу «Пользователи» и отладку **awg show**; **`GET /api/clients`** и остальные операции с клиентами **остаются** (скрыт только UI таблицы).
- **`mtproto`** или **`UI_HIDE_MTPROTO`** — скрывает блок MTProto; **`GET`/`POST /api/mtproto/*`** → 403.
## Конфигурации клиентов
- **Старые строки без `last_config`** — полный `.conf` с сервера собрать нельзя (нет приватного ключа). Используйте приложение Amnezia или блок **«Новый клиент под каскад»** (новый ключ на сервере).
- **Экспорт по кнопкам** — только если в `clientsTable` есть **`userData.last_config`** с полем `config` или `client_priv_key`.
- **Импорт из приложения Amnezia** — откройте блок **«Импорт клиента»**, выберите нужный **«Инстанс»**, вставьте клиентский `.conf` или JSON backup, где внутри есть такой `.conf`, и нажмите импорт. Если peer уже есть в `awg0.conf`, панель не создаёт дубль, а только восстанавливает/обновляет строку в `clientsTable`.
### Экспорт: имя файла и прямая ссылка
@@ -67,7 +94,7 @@
| Метод | Путь | Назначение |
|-------|------|------------|
| GET | `/health` | Проверка живости |
| GET | `/health` | Проверка живости; JSON **`{ ok, version }`** — **`version`** из `package.json` образа |
| GET | `/api/session` | Есть ли действующая сессия |
| POST | `/api/login` | `{ "password": "…" }` |
| POST | `/api/logout` | Выход |
@@ -82,10 +109,17 @@
| POST | `/api/clients/disconnect-date` | Даты отключения / расписание |
| GET/POST | `/api/clients/export-config` | Скачать `.conf`; GET — прямая ссылка (сессия); опционально `?token=…` если задан `EXPORT_CONFIG_SECRET` |
| POST | `/api/clients/create-cascade` | `{ "endpointHost", "endpointPort?", "tunnelIp?", "clientName?", "profileId?" }` — новый peer и файл `.conf` |
| POST | `/api/clients/import-config` | `{ "configText", "clientName?", "profileId?" }` — импорт клиента из `.conf` или JSON backup с вложенным `.conf` |
| POST | `/api/warp/host-setup` | Установка/удаление WARP на хосте по SSH: `{ "rootPassword", "cmd": "install" \| "uninstall" }` (как синхронизация времени; каталог скрипта — `WARP_SSH_INSTALL_DIR`) |
| POST | `/api/warp/start` | Поднять WARP |
| POST | `/api/warp/stop` | Остановить WARP |
| POST | `/api/warp/routing` | Политика по клиентам |
| GET | `/api/mtproto/status` | Состояние MTProto‑контейнера и **`tg://proxy`**; **`?withLogs=1`** — хвост логов (**`logsFetched`**) |
| GET | `/api/mtproto/logs` | Хвост **`docker logs`** прокси |
| GET | `/api/mtproto/tail` | То же для прокси, обрезающего путь **`…/logs`** |
| POST | `/api/mtproto/install` | Установить/обновить контейнер (тело `{}`) |
| POST | `/api/mtproto/restart` | Перезапустить контейнер |
| POST | `/api/mtproto/remove` | Удалить контейнер |
| GET | `/api/server-time` | Время и подсказки по поясам |
| GET | `/api/time-sync-capabilities` | Доступность синхронизации по SSH |
| POST | `/api/sync-host-time` | Запись времени на хост через SSH |

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

View File

@@ -21,6 +21,19 @@
</div>
</header>
<div class="proj-support-strip" role="navigation" aria-label="Поддержка и донат — проект панелей">
<div class="proj-support-inner">
<span class="proj-support-lead">Панели Amnezia Admin · поддержать проект:</span>
<a href="https://github.com/andrey271192/amnezia_web" target="_blank" rel="noopener noreferrer">⭐ GitHub</a>
<span class="proj-support-dot" aria-hidden="true">·</span>
<a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener noreferrer">💖 Boosty</a>
<span class="proj-support-dot" aria-hidden="true">·</span>
<a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener noreferrer">💳 Ozon&nbsp;СБП</a>
<span class="proj-support-dot" aria-hidden="true">·</span>
<a href="https://t.me/lot_andrey" target="_blank" rel="noopener noreferrer">✉️ Telegram&nbsp;@lot_andrey</a>
</div>
</div>
<section class="hero">
<div class="hero-badge">Частный сервер</div>
<h1>Подключение к VPN</h1>

View File

@@ -839,6 +839,45 @@ body {
text-align: right;
}
.proj-support-strip {
border-bottom: 1px solid var(--border);
background: linear-gradient(90deg, rgba(29, 158, 117, 0.12), rgba(55, 138, 221, 0.08));
padding: 0.4rem 1rem;
}
.proj-support-inner {
max-width: 900px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.35rem 0.65rem;
font-size: 12px;
color: var(--muted);
}
.proj-support-lead {
margin-right: 0.25rem;
opacity: 0.95;
}
.proj-support-dot {
opacity: 0.35;
user-select: none;
}
.proj-support-inner a {
color: #5fd8b0;
text-decoration: none;
white-space: nowrap;
font-weight: 600;
}
.proj-support-inner a:hover {
text-decoration: underline;
}
.site-footer {
border-top: 1px solid var(--border);
padding: 1.75rem 1.5rem 2rem;

828
package-lock.json generated Normal file
View File

@@ -0,0 +1,828 @@
{
"name": "amnezia-admin",
"version": "1.2.39",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "amnezia-admin",
"version": "1.2.39",
"license": "MIT",
"dependencies": {
"express": "^4.21.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.15.1",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "amnezia-admin",
"version": "1.1.7",
"private": true,
"version": "1.4.2",
"private": false,
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
"license": "MIT",
"repository": {

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,25 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<script>try{var t=localStorage.getItem("theme")||(matchMedia("(prefers-color-scheme: light)").matches?"light":"dark");document.documentElement.dataset.theme=t}catch(e){}</script>
<link rel="stylesheet" href="/styles.css?v=1.4.2">
</head>
<body class="page">
<aside class="panel-promo-strip" role="complementary" aria-label="Поддержка и донат">
<div class="panel-promo-strip-inner">
<p class="panel-promo-subtitle muted">Поддержите проект через Boosty, Ozon СБП и Telegram — ниже те же ссылки в подвале.</p>
<div class="panel-promo-links">
<a class="panel-promo-a" href="https://git.bobyrev.com/PCA/amnezia_web-PRO" target="_blank" rel="noopener noreferrer">⭐ Git</a>
<span class="panel-promo-sep" aria-hidden="true">·</span>
<a class="panel-promo-a" href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener noreferrer">💖 Boosty</a>
<span class="panel-promo-sep" aria-hidden="true">·</span>
<a class="panel-promo-a" href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener noreferrer">💳 Ozon&nbsp;СБП</a>
<span class="panel-promo-sep" aria-hidden="true">·</span>
<a class="panel-promo-a" href="https://t.me/lot_andrey" target="_blank" rel="noopener noreferrer">✉️ Telegram&nbsp;@lot_andrey</a>
</div>
</div>
</aside>
<div id="login-gate" class="gate hidden" aria-hidden="true">
<div class="gate-card">
<p class="eyebrow">Панель сервера</p>
@@ -21,6 +37,16 @@
<button type="submit" class="btn primary full">Войти</button>
<p id="login-error" class="status err" role="alert"></p>
</form>
<nav class="gate-donate muted" aria-label="Поддержка и донат">
Поддержка веб‑панелей (вне этого сервера):<br/>
<a href="https://git.bobyrev.com/PCA/amnezia_web-PRO" target="_blank" rel="noopener noreferrer">GitHub</a>
<span class="gate-donate-dot" aria-hidden="true">·</span>
<a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener noreferrer">Boosty</a>
<span class="gate-donate-dot" aria-hidden="true">·</span>
<a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener noreferrer">Ozon СБП</a>
<span class="gate-donate-dot" aria-hidden="true">·</span>
<a href="https://t.me/lot_andrey" target="_blank" rel="noopener noreferrer">Telegram</a>
</nav>
</div>
</div>
@@ -29,10 +55,11 @@
<div>
<p class="eyebrow">Панель сервера</p>
<h1>Пользователи AmneziaWG PRO</h1>
<p class="sub">Дату отключения задаёте сами (локальное время браузера). Кнопка «Выключить» убирает клиента из туннеля сразу. «Задать дату» без галочки — только запись в таблице; с галочкой — клиент остаётся в туннеле до наступления времени. Несколько инстансов задаются переменной <code class="inline">AWG_PROFILES</code>; установщик при апдейте подставляет её из прежнего контейнера или из <code class="inline">/root/amnezia-admin.awg-profiles.json</code>. Конфиг .conf — только если на сервере есть <code class="inline">last_config</code>; иначе используйте приложение Amnezia.</p>
<p class="sub">Дату отключения задаёте сами (локальное время браузера). Кнопка «Выключить» убирает клиента из туннеля сразу. «Задать дату» без галочки — только запись в таблице; с галочкой — клиент остаётся в туннеле до наступления времени. Несколько инстансов задаются переменной <code class="inline">AWG_PROFILES</code>; установщик при апдейте подставляет её из прежнего контейнера или из <code class="inline">/root/amnezia-admin.awg-profiles.json</code>. Конфиг .conf — только если на сервере есть <code class="inline">last_config</code>; иначе используйте приложение Amnezia. Отдельно: раздел <strong>Telegram MTProto‑прокси</strong> ставит официальный Docker‑образ <code class="inline">telegrammessenger/proxy</code> через панель.</p>
</div>
<div class="token-box">
<div class="session-actions">
<button type="button" id="theme-toggle" class="btn ghost full" title="Светлая / тёмная тема">◐ Тема</button>
<button type="button" id="logout" class="btn ghost full">Выйти</button>
</div>
<details class="pw-change">
@@ -51,7 +78,6 @@
</div>
</header>
<div id="edition-banner" class="edition-banner hidden" role="region" aria-label="Редакция панели"></div>
<section class="toolbar">
<div class="pill proto-pill"><span class="dot ok"></span><span id="proto-label">Протокол: AmneziaWG</span></div>
@@ -96,7 +122,7 @@
<summary class="fold-summary">
<span class="fold-arrow" aria-hidden="true"></span>
<span class="fold-titles">
<span class="fold-h">Cloudflare WARP</span>
<span class="fold-h">Cloudflare WARP / каскад</span>
<span class="muted fold-meta" id="warp-status-line"></span>
</span>
</summary>
@@ -107,6 +133,11 @@
Один раз на хосте VPS под root: в каталоге с репозиторием выполните <code class="inline">bash scripts/warp-amnezia.sh install</code>.
Затем отметьте клиентов ниже и нажмите «Применить» (контейнер AWG перезапустится). Установить или удалить WARP с хоста можно кнопками ниже (пароль root по SSH — как при синхронизации времени). Убрать вручную: <code class="inline">bash scripts/warp-amnezia.sh uninstall</code> — см. README.
</p>
<form id="upstream-form" class="cascade-form">
<label for="upstream-conf"><strong>Каскад через другой сервер</strong> — вставьте клиентский .conf (AmneziaWG или WireGuard) другого сервера. Он станет выходом вместо WARP: отмеченные ниже клиенты пойдут в интернет через тот сервер.</label>
<textarea id="upstream-conf" rows="6" placeholder="[Interface]&#10;PrivateKey = …&#10;[Peer]&#10;Endpoint = …" autocomplete="off"></textarea>
<button type="submit" class="btn primary">Подключить вышестоящий сервер</button>
</form>
<div id="warp-actions" class="warp-actions"></div>
<div id="warp-client-list" class="warp-client-list"></div>
<details class="raw warp-raw">
@@ -116,20 +147,209 @@
</div>
</details>
<details class="panel-fold mtproto-panel" id="mtproto-panel" hidden>
<summary class="fold-summary">
<span class="fold-arrow" aria-hidden="true"></span>
<span class="fold-titles">
<span class="fold-h">Telegram MTProto‑прокси</span>
<span class="muted fold-meta" id="mtproto-status-line"></span>
</span>
</summary>
<div class="panel-fold-body">
<p class="muted warp-intro mtproto-intro">
Отдельный Docker‑контейнер (<strong>telegrammessenger/proxy</strong>), публикация порта на хосте VPS.
Ссылка <code class="inline">tg://proxy?…</code> подставляет хост из URL панели, если не заданы переменные; для постоянного IP/DNS укажите <code class="inline">MTPRO_PUBLIC_HOST</code> или <code class="inline">CLIENT_CONFIG_ENDPOINT</code>.
Секрет после установки сохраните вне панели — здесь он показывается маскированно.
За nginx/Caddy проксируйте префикс <code class="inline">/api/</code> целиком (см. README).
</p>
<p id="mtproto-callout" class="mtproto-callout hidden" role="note"></p>
<div id="mtproto-link-card" class="mtproto-link-card hidden" role="region" aria-label="Ссылка для Telegram">
<div class="mtproto-link-row">
<strong class="mtproto-link-label">Ссылка в Telegram</strong>
<a id="mtproto-link-anchor" class="mtproto-link-anchor" target="_blank" rel="noopener noreferrer"></a>
</div>
<div class="mtproto-link-row mtproto-link-actions">
<button type="button" id="mtproto-link-copy" class="btn small ghost">Скопировать</button>
<span id="mtproto-link-copy-state" class="muted warp-muted"></span>
</div>
</div>
<div class="mtproto-fields">
<label for="mtproto-host-port">Порт хоста (необязательно — по умолчанию см. MTPRO_PUBLISH_PORT, обычно 8443)</label>
<input id="mtproto-host-port" type="number" min="512" max="65535" autocomplete="off" placeholder="8443 или другой свободный порт">
<label for="mtproto-secret-opt">Кастомный секрет hex (32 символа, необязательно)</label>
<input id="mtproto-secret-opt" type="text" maxlength="32" autocomplete="off" placeholder="Пусто — сгенерируется">
</div>
<div id="mtproto-actions" class="warp-actions"></div>
<p id="mtproto-banner" class="status ok hidden" role="status"></p>
<details class="raw warp-raw">
<summary>Хвост логов контейнера mtproto‑proxy</summary>
<pre id="mtproto-logs-tail"></pre>
</details>
</div>
</details>
<details class="panel-fold" id="xray-panel">
<summary class="fold-summary">
<span class="fold-arrow" aria-hidden="true"></span>
<span class="fold-titles">
<span class="fold-h">Xray VLESS‑Reality</span>
<span class="muted fold-meta" id="xray-status-line"></span>
</span>
</summary>
<div class="panel-fold-body">
<p class="muted warp-intro">
Протокол для мест, где блокируют AmneziaWG: трафик выглядит как обычный HTTPS к сайту‑маскировке.
Ссылку <code class="inline">vless://</code> вставьте в Amnezia VPN, v2rayNG, Hiddify, Streisand или Happ. Нужен открытый TCP‑порт.
</p>
<form id="xray-install-form" class="instance-form">
<div class="instance-form-row">
<label for="xray-port">Порт (TCP)</label>
<input id="xray-port" type="number" min="1" max="65535" value="443">
</div>
<div class="instance-form-row">
<label for="xray-sni">Сайт‑маскировка</label>
<input id="xray-sni" type="text" value="www.google.com" autocomplete="off">
</div>
<button type="submit" class="btn primary">+ Установить Xray</button>
</form>
<div id="xray-manage" style="display:none">
<form id="xray-client-form" class="instance-form">
<div class="instance-form-row">
<label for="xray-client-name">Новый клиент</label>
<input id="xray-client-name" type="text" maxlength="64" placeholder="Имя" autocomplete="off">
</div>
<button type="submit" class="btn primary">+ Создать</button>
<button type="button" id="xray-remove" class="btn small warn">Удалить Xray</button>
</form>
<table class="clients-table"><tbody id="xray-rows"></tbody></table>
</div>
</div>
</details>
<details class="panel-fold" id="instances-panel" open>
<summary class="fold-summary">
<span class="fold-arrow" aria-hidden="true"></span>
<span class="fold-titles">
<span class="fold-h">Протоколы / инстансы</span>
<span class="muted fold-meta">Разверните AmneziaWG прямо из панели — без приложения Amnezia, с выбором порта</span>
</span>
</summary>
<div class="panel-fold-body">
<div id="instances-grid" class="instances-grid"></div>
<form id="instance-form" class="instance-form">
<div class="instance-form-row">
<label for="instance-variant">Протокол</label>
<select id="instance-variant" class="form-select">
<option value="awg2">AmneziaWG 2.0 (рекомендуется)</option>
<option value="awg31">AmneziaWG 3.1 — новейший, свежее приложение Amnezia</option>
<option value="awg3">AmneziaWG 3.0</option>
<option value="awg">AmneziaWG 1.x — роутеры Keenetic/OpenWrt, старые приложения</option>
<option value="legacy">WireGuard (Legacy) — любой WG-клиент, без обфускации</option>
</select>
</div>
<div class="instance-form-row">
<label for="instance-port">Порт (UDP)</label>
<input id="instance-port" type="number" min="1" max="65535" placeholder="напр. 51820" autocomplete="off">
</div>
<button type="submit" class="btn primary">+ Развернуть инстанс</button>
</form>
<p class="muted instance-hint">Создаёт серверный контейнер AmneziaWG из публичного образа, генерирует ключи и NAT. Затем в «Пользователях» выберите инстанс и создавайте клиентов. Можно держать несколько версий рядом на разных портах: 3.1/2.0 — для телефонов и ПК, 1.x или WireGuard — для роутеров. Не забудьте открыть UDP-порт в фаерволе хостинга.</p>
<form id="settings-form" class="instance-form">
<div class="instance-form-row">
<label for="settings-endpoint">Домен или IP сервера в конфигах клиентов</label>
<input id="settings-endpoint" type="text" placeholder="vpn.example.com — пусто: IP из установки" autocomplete="off">
</div>
<button type="submit" class="btn primary">Сохранить адрес</button>
</form>
<p id="settings-hint" class="muted instance-hint"></p>
</div>
</details>
<details class="panel-fold" id="direct-panel" open>
<summary class="fold-summary">
<span class="fold-arrow" aria-hidden="true"></span>
<span class="fold-titles">
<span class="fold-h">Новый клиент</span>
<span class="muted fold-meta">Прямое подключение к этому серверу — Endpoint = публичный IP этого VPS</span>
</span>
</summary>
<div class="panel-fold-body">
<div class="muted cascade-guide">
<p class="cascade-intro">
<strong>Обычное подключение.</strong> Создаёт нового клиента AmneziaWG и сразу скачивает файл
<code class="inline">.conf</code>, где <strong>Endpoint указывает на этот сервер напрямую</strong>.
Импортируйте файл в приложение Amnezia на телефоне или ПК. IP в туннеле подберётся автоматически.
</p>
</div>
<form id="direct-form" class="cascade-form">
<label for="direct-name">Имя клиента</label>
<input id="direct-name" type="text" placeholder="Например iPhone Андрей" autocomplete="off">
<label for="direct-tunnel-ip">IP в VPN-туннеле (необязательно)</label>
<input id="direct-tunnel-ip" type="text" placeholder="Пусто — подберём свободный в подсети" autocomplete="off">
<button type="submit" class="btn primary">Создать и скачать .conf</button>
</form>
</div>
</details>
<details class="panel-fold cascade-panel" id="import-panel">
<summary class="fold-summary">
<span class="fold-arrow" aria-hidden="true"></span>
<span class="fold-titles">
<span class="fold-h">Импорт клиента</span>
<span class="muted fold-meta">Перенос из готового .conf или backup Amnezia, где внутри есть конфиг</span>
</span>
</summary>
<div class="panel-fold-body">
<div class="muted cascade-guide">
<p class="cascade-intro">
Вставьте содержимое клиентского <code class="inline">.conf</code> или JSON backup. Панель добавит peer в текущий инстанс,
восстановит запись в <code class="inline">clientsTable</code> и сохранит исходный конфиг для дальнейшего экспорта.
</p>
</div>
<form id="import-form" class="cascade-form import-form">
<label for="import-name">Имя клиента / префикс</label>
<input id="import-name" type="text" placeholder="Например iPhone из backup" autocomplete="off">
<label for="import-config">.conf или JSON backup</label>
<textarea id="import-config" rows="10" spellcheck="false" placeholder="[Interface]&#10;PrivateKey = ...&#10;Address = 10.8.1.10/32&#10;&#10;[Peer]&#10;PublicKey = ..."></textarea>
<button type="submit" class="btn primary">Импортировать в текущий инстанс</button>
</form>
</div>
</details>
<details class="panel-fold cascade-panel" id="cascade-panel" open>
<summary class="fold-summary">
<span class="fold-arrow" aria-hidden="true"></span>
<span class="fold-titles">
<span class="fold-h">Новый клиент под каскад</span>
<span class="muted fold-meta">Создать peer и скачать .conf</span>
<span class="muted fold-meta">Клиент стучится в «первый» узел → трафик сюда; ключи выдаём на этом VPS</span>
</span>
</summary>
<div class="panel-fold-body">
<p class="muted cascade-intro">
Укажите <strong>Endpoint</strong> — публичный IP или DNS узла, <em>куда клиент будет подключаться первым шагом</em>
(промежуточный сервер, домашний роутер с пробросом порта и т.д.). Порт по умолчанию совпадает с <code class="inline">ListenPort</code> этого инстанса.
Клиент будет <strong>создан на текущем сервере AmneziaWG</strong> (новые ключи); IP в туннеле можно задать явно или оставить пустым — подберём свободный в той же подсети, что у остальных.
<div class="muted cascade-guide">
<p class="cascade-intro">
<strong>Что здесь происходит.</strong> Приложение Amnezia всегда смотрит на поле <strong>Endpoint</strong> в конфиге.
При каскаде клиент «видит» только <strong>промежуточный узел</strong> (часто недорогой VPS поближе к пользователям), а <strong>реальное соединение AmneziaWG живёт на этом сервере</strong>, где крутится ваш основной контейнер.
Ключ и peer создаём здесь и выдаём файл <code class="inline">.conf</code> — Endpoint в файле будет на промежуточную машину, а не на прямой IP этого VPS в интернете.
</p>
<p class="cascade-intro cascade-intro-tight"><strong>Сначала нужна пересылка порта до этого VPS.</strong> На промежуточном сервере настройте проброс DNAT UDP (и TCP при необходимости).
Удобный вариант с веб-панелью iptables —
репозиторий <a href="https://github.com/andrey271192/kaskad_web_vpn" target="_blank" rel="noopener noreferrer"><strong>kaskad_web_vpn</strong></a>:
ставится одной командой ниже; в её панели создаёте правило «входящий порт → UDP на IP:порт этого AmneziaWG». Подробнее — README в том же репозитории.</p>
<pre class="cascade-snippet" tabindex="0" role="note" aria-label="Установка kaskad_web_vpn на промежуточном VPS">curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad_web_vpn/main/install.sh | sudo bash</pre>
<p class="cascade-intro cascade-intro-tight">После установки откройте <code class="inline">http://ПЕРЕХОДНОЙ_IP:8088/setup</code> (или свой <code class="inline">HOST_PORT</code>), задайте пароль админки каскада и добавьте правило проброса на <strong><code class="inline">ListenPort</code> этого инстанса</strong> на данном VPS (видно в шапке панели или в <code class="inline">awg show</code>).</p>
<ol class="cascade-steps">
<li>На промежуточном VPS: <strong>kaskad_web_vpn</strong> (команда выше) или свой DNAT / проброс порта на роутере.</li>
<li>Проверьте, что с промежуточной машины доходит UDP до WG-порта на <strong>этом</strong> сервере (если ниже поле порта Endpoint пустое — это тот же порт, что <code class="inline">ListenPort</code> здесь).</li>
<li>В форме ниже в <strong>Endpoint</strong> укажите публичный <strong>IP или DNS промежуточного узла</strong> — тот адрес, с которым реально установит туннель клиентское приложение из интернета.</li>
<li>Нажмите <strong>Создать на сервере и скачать .conf</strong> и импортируйте файл в Amnezia на телефоне или ПК.</li>
</ol>
<p class="cascade-intro cascade-intro-tight"><strong>Порт Endpoint</strong> — когда снаружи слушает не тот номер порта, что WG здесь (пусто значит «как у сервера»). <strong>IP в VPN-туннеле</strong> можно не заполнять — подберём свободный адрес в той же подсети, что у остальных клиентов.</p>
</div>
<form id="cascade-form" class="cascade-form">
<label for="cascade-endpoint">Endpoint (хост для клиента)</label>
<input id="cascade-endpoint" type="text" placeholder="Например 203.0.113.50 или ddns.example.com" required autocomplete="off">
@@ -157,6 +377,10 @@
</span>
</summary>
<div class="panel-fold-body">
<div class="users-tools">
<button id="sync-peers-btn" class="btn small ghost" type="button">Подтянуть из awg0.conf</button>
<span class="muted users-tools-hint">Для клиентов, созданных в приложении Amnezia</span>
</div>
<div class="table-wrap">
<table class="clients-table">
<thead>
@@ -189,7 +413,7 @@
<div class="support-line">
<strong class="support-title">amnezia_web-PRO</strong>
<span class="support-sep" aria-hidden="true">·</span>
<a href="https://github.com/andrey271192/amnezia_web-PRO" target="_blank" rel="noopener noreferrer">⭐ GitHub · amnezia_web-PRO</a>
<a href="https://git.bobyrev.com/PCA/amnezia_web-PRO" target="_blank" rel="noopener noreferrer">⭐ Git · amnezia_web-PRO</a>
<span class="support-sep" aria-hidden="true">·</span>
<a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener noreferrer">💖 Boosty</a>
<span class="support-sep" aria-hidden="true">·</span>
@@ -236,6 +460,6 @@
</div>
</dialog>
<script src="/app.js" type="module"></script>
<script src="/app.js?v=1.4.2" type="module"></script>
</body>
</html>

View File

@@ -8,6 +8,10 @@
--danger: #fb7185;
--ok: #4ade80;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
--field: #0a0f16;
--text-soft: #cbd5f5;
--overlay: rgba(10, 15, 22, 0.92);
color-scheme: dark;
font-family: "DM Sans", system-ui, sans-serif;
}
@@ -15,6 +19,28 @@
box-sizing: border-box;
}
/* Светлая тема: те же переменные, переключатель в шапке (localStorage "theme"). */
html[data-theme="light"] {
--bg: #f4f6fb;
--card: #ffffff;
--line: rgba(15, 23, 42, 0.12);
--text: #0f172a;
--muted: #526077;
--accent: #0369a1;
--danger: #be123c;
--ok: #15803d;
--shadow: 0 12px 40px rgba(15, 23, 42, 0.12);
--field: #ffffff;
--text-soft: #1e293b;
--overlay: rgba(255, 255, 255, 0.94);
color-scheme: light;
}
html[data-theme="light"] .panel-promo-strip {
background: linear-gradient(90deg, #e0f2fe, #eef2ff);
border-bottom-color: rgba(3, 105, 161, 0.25);
}
html[data-theme="light"] .panel-promo-a { color: #92400e; }
body.page {
margin: 0;
background: radial-gradient(circle at 20% 20%, rgba(125, 211, 252, 0.08), transparent 35%),
@@ -26,6 +52,59 @@ body.page {
flex-direction: column;
}
.panel-promo-strip {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 40;
border-bottom: 1px solid rgba(125, 211, 252, 0.28);
background: linear-gradient(90deg, rgba(30, 41, 59, 0.94), rgba(15, 23, 42, 0.96));
backdrop-filter: blur(8px);
}
.panel-promo-strip-inner {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1.25rem;
max-width: min(1120px, 100%);
margin: 0 auto;
padding: 0.55rem clamp(1rem, 3vw, 1.5rem);
}
.panel-promo-subtitle {
margin: 0;
flex: 1 1 16rem;
font-size: 0.82rem;
line-height: 1.45;
}
.panel-promo-links {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem 0.65rem;
}
.panel-promo-a {
font-weight: 650;
font-size: 0.86rem;
color: #fde68a;
text-decoration: none;
white-space: nowrap;
}
.panel-promo-a:hover {
text-decoration: underline;
}
.panel-promo-sep {
color: var(--muted);
font-weight: 700;
user-select: none;
}
.shell {
flex: 1 0 auto;
max-width: min(1120px, 100%);
@@ -70,7 +149,7 @@ h1 {
border: 1px solid var(--line);
border-radius: 14px;
padding: 1rem;
background: #0c121b;
background: var(--field);
}
.token-box label {
@@ -138,12 +217,33 @@ h1 {
padding: 0.65rem 0.75rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
margin-bottom: 0.75rem;
}
.gate-donate {
margin-top: 1rem;
padding-top: 0.9rem;
border-top: 1px dashed rgba(148, 163, 184, 0.45);
font-size: 0.84rem;
line-height: 1.65;
}
.gate-donate a {
color: var(--accent);
font-weight: 650;
white-space: nowrap;
}
.gate-donate-dot {
display: inline-block;
opacity: 0.45;
margin: 0 0.2rem;
user-select: none;
}
.btn.full {
width: 100%;
margin-top: 0.25rem;
@@ -181,7 +281,7 @@ h1 {
padding: 0.55rem 0.65rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
}
@@ -191,7 +291,7 @@ h1 {
padding: 0.65rem 0.75rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 0.85rem;
@@ -220,7 +320,7 @@ h1 {
padding: 0.45rem 0.55rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
font-size: 0.85rem;
@@ -333,7 +433,7 @@ h1 {
padding: 0.45rem 0.55rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
font-size: 0.85rem;
@@ -505,7 +605,7 @@ h1 {
}
.panel-fold-body > .warp-intro:first-child,
.panel-fold-body > .cascade-intro:first-child {
.panel-fold-body > .cascade-guide:first-child {
margin-top: 0.85rem;
}
@@ -551,6 +651,18 @@ h1 {
font-size: 0.88rem;
}
.users-tools {
display: flex;
align-items: center;
gap: 0.65rem;
flex-wrap: wrap;
margin: 0 0 0.75rem;
}
.users-tools-hint {
font-size: 0.82rem;
}
table.clients-table {
table-layout: fixed;
}
@@ -582,6 +694,8 @@ table.clients-table {
.clients-table td.actions {
vertical-align: top;
width: 1%;
white-space: nowrap;
}
table {
@@ -627,7 +741,7 @@ tr:last-child td {
.ip {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 0.85rem;
color: #dbeafe;
color: var(--text-soft);
}
.badge {
@@ -648,15 +762,22 @@ tr:last-child td {
.badge.off {
border-color: rgba(148, 163, 184, 0.35);
color: #e2e8f0;
color: var(--text-soft);
background: rgba(148, 163, 184, 0.06);
}
.actions {
display: flex;
gap: 0.35rem;
gap: 0.4rem;
justify-content: flex-end;
flex-wrap: wrap;
align-items: center;
}
.actions .btn,
.actions a.btn {
white-space: nowrap;
flex: 0 0 auto;
}
.export-missing-hint {
@@ -702,6 +823,53 @@ tr:last-child td {
font-size: 0.85em;
}
.cascade-guide .cascade-intro {
margin: 0 0 0.75rem;
line-height: 1.5;
}
.cascade-guide .cascade-intro-tight {
margin-bottom: 0.55rem;
}
.cascade-guide a {
color: #7dd3fc;
text-decoration: underline;
text-underline-offset: 0.14em;
}
.cascade-guide a:hover {
color: #bae6fd;
}
.cascade-steps {
margin: 0.65rem 0 0.85rem;
padding-left: 1.35rem;
line-height: 1.52;
}
.cascade-steps li {
margin-bottom: 0.45rem;
}
.cascade-steps li:last-child {
margin-bottom: 0;
}
.cascade-snippet {
white-space: pre-wrap;
word-break: break-word;
background: var(--field);
border: 1px solid var(--line);
padding: 0.75rem 0.95rem;
border-radius: 12px;
color: var(--text-soft);
font-size: 0.78rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
margin: 0.5rem 0 0.85rem;
max-width: min(52rem, 100%);
}
.cascade-form {
display: grid;
gap: 0.55rem;
@@ -714,10 +882,22 @@ tr:last-child td {
color: var(--muted);
}
.cascade-form input {
.cascade-form input,
.cascade-form textarea {
width: 100%;
}
.cascade-form textarea {
resize: vertical;
min-height: 12rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
line-height: 1.45;
}
.import-form {
max-width: 46rem;
}
.raw {
margin-top: 1.25rem;
color: var(--muted);
@@ -730,11 +910,11 @@ tr:last-child td {
.raw pre {
white-space: pre-wrap;
word-break: break-word;
background: #0a0f16;
background: var(--field);
border: 1px solid var(--line);
padding: 0.75rem;
border-radius: 12px;
color: #cbd5f5;
color: var(--text-soft);
font-size: 0.78rem;
}
@@ -773,7 +953,7 @@ tr:last-child td {
padding: 0.55rem 0.65rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
}
@@ -821,7 +1001,7 @@ tr:last-child td {
margin-top: auto;
padding: 1.25rem clamp(1rem, 3vw, 1.5rem) 1.5rem;
border-top: 1px solid var(--line);
background: rgba(10, 15, 22, 0.92);
background: var(--overlay);
}
.support-line {
@@ -917,36 +1097,138 @@ tr:last-child td {
overflow: auto;
}
.edition-banner {
margin: 0 0 1rem;
padding: 0.85rem 1rem;
border-radius: 14px;
border: 1px solid rgba(125, 211, 252, 0.35);
background: linear-gradient(135deg, rgba(125, 211, 252, 0.12), rgba(94, 234, 212, 0.06));
/* --- Telegram MTProto --- */
.mtproto-link-card {
margin: 0.65rem 0 0.85rem;
padding: 0.75rem 0.9rem;
border-radius: 12px;
border: 1px solid rgba(125, 211, 252, 0.45);
background: rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.edition-banner.hidden {
.mtproto-link-card.hidden {
display: none;
}
.edition-banner-inner {
.mtproto-link-row {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1rem;
align-items: center;
justify-content: space-between;
gap: 0.4rem 0.75rem;
}
.edition-banner-text {
flex: 1 1 280px;
.mtproto-link-label {
color: var(--text);
font-weight: 700;
}
.edition-banner-pitch {
.mtproto-link-anchor {
flex: 1 1 auto;
word-break: break-all;
color: var(--accent);
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 0.84rem;
}
.mtproto-panel .warp-intro.mtproto-intro {
margin-top: 0;
}
.mtproto-fields {
display: grid;
gap: 0.35rem 0.85rem;
max-width: 28rem;
margin: 0.5rem 0 1rem;
}
.mtproto-fields label {
font-size: 0.82rem;
color: var(--muted);
}
.mtproto-fields input {
padding: 0.45rem 0.65rem;
border-radius: 8px;
border: 1px solid var(--line);
background: rgba(10, 15, 22, 0.55);
color: var(--text);
font: inherit;
}
.mtproto-deep-link strong {
display: inline-block;
margin-right: 0.35rem;
}
.mtproto-deep-link a {
word-break: break-all;
}
.mtproto-callout {
margin: 0.35rem 0 0;
font-size: 0.88rem;
line-height: 1.45;
padding: 0.65rem 0.85rem;
border-radius: 12px;
border: 1px solid rgba(125, 211, 252, 0.4);
background: rgba(125, 211, 252, 0.1);
color: var(--text);
font-size: 0.86rem;
line-height: 1.52;
}
.edition-banner-cta {
flex-shrink: 0;
#mtproto-banner {
margin: 0.75rem 0 0;
white-space: pre-wrap;
font-size: 0.82rem;
}
.actions { gap: 0.3rem; flex-wrap: nowrap; }
.actions .btn.icon {
width: 32px;
min-width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1rem;
line-height: 1;
}
.actions a.btn.icon { text-decoration: none; }
/* Managed instances */
.instances-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; margin-bottom: 16px; }
.inst-card { background: var(--card, rgba(148,163,184,0.06)); border: 1px solid var(--line); border-radius: 16px; padding: 16px; display: flex; flex-direction: column; gap: 8px; }
.inst-card-head { display: flex; align-items: center; justify-content: space-between; }
.inst-ico { width: 44px; height: 44px; border-radius: 12px; background: rgba(124,92,255,0.14); display: flex; align-items: center; justify-content: center; font-size: 22px; }
.inst-title { font-weight: 700; font-size: 1.05rem; }
.inst-badge { font-size: 0.62rem; background: #7c5cff; color: #fff; border-radius: 999px; padding: 2px 7px; vertical-align: middle; letter-spacing: 0.05em; }
.inst-desc { font-size: 0.82rem; line-height: 1.35; min-height: 2.4em; }
.inst-status.on { color: #34c759; font-size: 0.72rem; font-weight: 700; }
.inst-status.off { color: #ff9f0a; font-size: 0.72rem; font-weight: 700; }
.inst-status-row { margin: 2px 0; }
.inst-stats { display: flex; gap: 18px; margin: 4px 0; }
.inst-stat-l { display: block; font-size: 0.62rem; color: var(--muted); letter-spacing: 0.06em; }
.inst-stat-v { display: block; font-weight: 700; font-size: 0.95rem; }
.inst-actions { display: flex; gap: 8px; margin-top: 6px; }
.inst-actions .btn { flex: 1; }
.instance-form { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-top: 8px; }
.instance-form-row { display: flex; flex-direction: column; gap: 4px; }
.instance-form-row label { font-size: 0.75rem; color: var(--muted); }
.instance-hint { font-size: 0.75rem; margin-top: 8px; }
.instance-empty { padding: 12px; }
.inst-src { font-size: 0.6rem; color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 2px 7px; vertical-align: middle; }
/* Светлая тема: пастельные надписи тёмной темы на белом не читаются */
html[data-theme="light"] .clock-zone-diff--accent,
html[data-theme="light"] .cascade-guide a,
html[data-theme="light"] .cascade-guide a:hover { color: #0369a1; }
html[data-theme="light"] .btn.warn { color: #9f1239; }
html[data-theme="light"] .status.err { color: #b91c1c; }
html[data-theme="light"] .badge.on { color: #166534; }
html[data-theme="light"] body.page { background: var(--bg); }

195
scripts/awg-instance.sh Executable file
View File

@@ -0,0 +1,195 @@
#!/usr/bin/env bash
# Deploy / remove an AmneziaWG server instance from public images — no Amnezia app.
# Variants: awg31 (AmneziaWG 3.1), awg3 (3.0), awg2 (2.0, awg-go 2.0.0),
# awg (1.x for routers/old apps, awg-go 0.2.18), legacy (plain WireGuard, amnezia-wg).
# Usage:
# awg-instance.sh create <variant> <port> [name]
# awg-instance.sh remove <name>
# awg-instance.sh list
set -euo pipefail
INSTANCES_DIR="${INSTANCES_DIR:-/opt/amnezia-instances}"
IMG_AWG2="${IMG_AWG2:-amneziavpn/amneziawg-go:2.0.0}"
IMG_AWG="${IMG_AWG:-amneziavpn/amneziawg-go:0.2.18}"
IMG_LEGACY="${IMG_LEGACY:-amneziavpn/amnezia-wg:latest}"
# 3.x pinned by digest: tags on Docker Hub get rewritten (tools inside differ from the tag name)
IMG_AWG3="${IMG_AWG3:-amneziavpn/amneziawg-go:3.0.20260805@sha256:8447c91637c37536dd99b8bbd4420c819ac9f330f047804197291625bfb0ea8a}"
IMG_AWG31="${IMG_AWG31:-amneziavpn/amneziawg-go:3.1.20260828@sha256:cbafc02b8373a83f428272db6d8001b37bc02e6211cbd8c0cb4e2e3759b12b72}"
err() { echo "ERROR: $*" >&2; exit 1; }
rand() { od -An -N4 -tu4 </dev/urandom | tr -d ' '; }
rand_range() { # min max
local min="$1"
local max="$2"
local span=$(( max - min + 1 ))
echo $(( min + ($(rand) % span) ))
}
rand_magic() { # single large uint32 (AmneziaWG H header), NOT a range
rand_range 1000000000 2147000000
}
image_for() {
case "$1" in
awg31) echo "$IMG_AWG31" ;;
awg3) echo "$IMG_AWG3" ;;
awg2) echo "$IMG_AWG2" ;;
awg) echo "$IMG_AWG" ;;
legacy) echo "$IMG_LEGACY" ;;
*) err "unknown variant: $1 (awg31|awg3|awg2|awg|legacy)" ;;
esac
}
# pick an unused 10.8.<N>.0/24 subnet
pick_subnet() {
local used n c
used="$(grep -rhoE 'Address = 10\.8\.[0-9]+\.' "$INSTANCES_DIR"/*/conf/*.conf 2>/dev/null | grep -oE '10\.8\.[0-9]+' | awk -F. '{print $3}' | sort -un || true)"
# also subnets used by any running container (native Amnezia, etc.)
for c in $(docker ps --format '{{.Names}}' 2>/dev/null); do
local a
a="$(docker exec "$c" sh -c 'cat /opt/amnezia/awg/*.conf 2>/dev/null' 2>/dev/null | grep -oE 'Address = 10\.8\.[0-9]+' | grep -oE '10\.8\.[0-9]+' | awk -F. '{print $3}' || true)"
[[ -n "$a" ]] && used="$used"$'\n'"$a"
done
used="$(printf '%s\n' "$used" | sort -un)"
# start at 20 to avoid colliding with Amnezia native default 10.8.1
for n in $(seq 20 250); do
if ! grep -qx "$n" <<<"$used"; then echo "$n"; return; fi
done
err "no free subnet"
}
cmd_create() {
local variant="$1" port="$2" name="${3:-}"
[[ "$port" =~ ^[0-9]+$ ]] && (( port>=1 && port<=65535 )) || err "bad port: $port"
local img; img="$(image_for "$variant")"
[[ -z "$name" ]] && name="amnezia-${variant}-${port}"
# sanitize name
name="$(echo "$name" | tr -cd 'a-zA-Z0-9_-')"
[[ -n "$name" ]] || err "bad name"
docker inspect "$name" >/dev/null 2>&1 && err "container $name already exists"
# port free?
# this script runs inside the panel container: its own `ss` does not see host ports, docker does
if docker ps --format '{{.Ports}}' | tr ',' '\n' | grep -qE ":${port}->[0-9]+/udp"; then err "udp port $port busy (другой контейнер)"; fi
echo "→ pulling $img"
docker pull -q "$img" >/dev/null
local sub; sub="$(pick_subnet)"
local net="10.8.${sub}"
local dir="$INSTANCES_DIR/$name/conf"
mkdir -p "$dir"
local conf binary iface
if [[ "$variant" == "legacy" ]]; then binary="wg"; iface="wg0"; else binary="awg"; iface="awg0"; fi
conf="$dir/${iface}.conf"
# keys via the image
local priv pub psk
priv="$(docker run --rm "$img" "$binary" genkey | tr -d '\r\n')"
pub="$(printf '%s' "$priv" | docker run --rm -i "$img" "$binary" pubkey | tr -d '\r\n')"
psk="$(docker run --rm "$img" "$binary" genpsk | tr -d '\r\n')"
# interface block
{
echo "[Interface]"
echo "PrivateKey = $priv"
echo "Address = ${net}.1/24"
echo "ListenPort = $port"
if [[ "$variant" != "legacy" ]]; then
# 3.x header protection rejects junk sizes <= 12
local smin=15
echo "Jc = $(rand_range 3 10)"
echo "Jmin = 10"
echo "Jmax = 50"
echo "S1 = $(rand_range $smin 60)"
echo "S2 = $(rand_range $smin 60)"
if [[ "$variant" != "awg" ]]; then
echo "S3 = $(rand_range $smin 40)"
echo "S4 = $(rand_range $smin 40)"
fi
echo "H1 = $(rand_magic)"
echo "H2 = $(rand_magic)"
echo "H3 = $(rand_magic)"
echo "H4 = $(rand_magic)"
if [[ "$variant" == awg3* ]]; then
echo "HeaderProtectionKey = $(docker run --rm "$img" awg genkey | tr -d '\r\n')"
echo "ContentPaddingAddition = 10-100"
echo "RekeyAfterTime = 100-120"
echo "RekeyTimeout = 3-7"
echo "RejectAfterTime = 150-180"
echo "KeepaliveTimeout = 5-15"
echo "MaxHandshakeAttempts = 15-20"
fi
if [[ "$variant" == "awg31" ]]; then
echo "RandomTrailers = on"
echo "DisableCookies = on"
fi
fi
} > "$conf"
printf '%s\n' "$pub" > "$dir/wireguard_server_public_key.key"
printf '%s\n' "$priv" > "$dir/wireguard_server_private_key.key"
printf '%s\n' "$psk" > "$dir/wireguard_psk.key"
printf '[]\n' > "$dir/clientsTable"
# start script run inside container
local usimpl=""
if [[ "$variant" != "legacy" ]]; then usimpl="export WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go"; fi
# 3.x is userspace-only: a loaded amneziawg kernel module (2.0 on the same host) does not know 3.x keys
if [[ "$variant" == awg3* ]]; then
usimpl="$usimpl"$'\n'"$(cat <<'PATCH'
sed -i -e 's/if ! cmd ip link add "$INTERFACE" type amneziawg; then/if true; then/' -e 's|^\([[:space:]]*\)\[\[ -e /sys/module/amneziawg \]\].*|\1:|' /usr/bin/awg-quick
PATCH
)"
fi
cat > "$dir/start.sh" <<EOF
#!/bin/sh
set +e
$usimpl
${binary}-quick down /opt/amnezia/awg/${iface}.conf 2>/dev/null
[ -f /opt/amnezia/awg/${iface}.conf ] && ${binary}-quick up /opt/amnezia/awg/${iface}.conf
DEV=\$(ip route 2>/dev/null | awk '/default/ {print \$5; exit}')
[ -z "\$DEV" ] && DEV=eth0
iptables -A INPUT -i ${iface} -j ACCEPT
iptables -A FORWARD -i ${iface} -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -A POSTROUTING -s ${net}.0/24 -o "\$DEV" -j MASQUERADE
exec tail -f /dev/null
EOF
chmod +x "$dir/start.sh"
echo "→ run container $name (port $port/udp, subnet ${net}.0/24)"
docker run -d --name "$name" --restart unless-stopped \
--cap-add NET_ADMIN --cap-add SYS_MODULE --privileged \
--sysctl net.ipv4.conf.all.src_valid_mark=1 \
-p "${port}:${port}/udp" \
-v /lib/modules:/lib/modules:ro \
-v "$dir:/opt/amnezia/awg" \
"$img" sh /opt/amnezia/awg/start.sh >/dev/null
sleep 2
if docker exec "$name" "$binary" show "$iface" >/dev/null 2>&1; then
echo "OK container=$name variant=$variant port=$port subnet=${net}.0/24 binary=$binary iface=$iface conf=$conf"
else
echo "WARN container started but '$binary show $iface' failed — check: docker logs $name"
echo "OK_PARTIAL container=$name variant=$variant port=$port"
fi
}
cmd_remove() {
local name="$1"
docker rm -f "$name" >/dev/null 2>&1 || true
rm -rf "${INSTANCES_DIR:?}/$name"
echo "removed $name"
}
cmd_list() {
docker ps --format '{{.Names}} {{.Ports}}' | grep -E '^amnezia-' || true
}
case "${1:-}" in
create) shift; cmd_create "$@" ;;
remove) shift; cmd_remove "$@" ;;
list) cmd_list ;;
*) err "usage: $0 {create <variant> <port> [name] | remove <name> | list}" ;;
esac

53
scripts/checks/audit2.py Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Сверяем ПОДНЯТЫЕ интерфейсы с их собственными конфигами.
Первый заход сравнивал wg0.conf с интерфейсом wg0, а в контейнере amnezia-awg
поднят awg0 — отсюда ложные «расхождения».
"""
import json, sys
sys.path.insert(0, "/app")
from managers.ssh_manager import SSHManager
PROBE = r"""
for c in $(docker ps --format '{{.Names}}' 2>/dev/null | grep '^amnezia-' | grep -vE 'nginx|frontend|backend|landing|xray|socks|ipsec|openvpn'); do
for bin in awg wg; do
docker exec $c sh -c "command -v $bin >/dev/null" 2>/dev/null || continue
for i in $(docker exec $c sh -c "$bin show interfaces 2>/dev/null"); do
live=$(docker exec $c sh -c "$bin show $i peers 2>/dev/null | grep -c ." | tr -d '\n')
conf=""
for f in /opt/amnezia/awg/$i.conf /opt/amnezia/awg/awg0.conf /opt/amnezia/awg/wg0.conf; do
docker exec $c test -f $f 2>/dev/null && { conf=$f; break; }
done
[ -n "$conf" ] || { echo "$c $i live=$live конфиг=НЕ НАЙДЕН"; continue; }
infile=$(docker exec $c grep -c '^\[Peer\]' $conf 2>/dev/null | tr -d '\n')
echo "$c $i live=$live файл=$infile ($conf)"
done
done
done
"""
d = json.load(open("/app/data/data.json"))
плохо = 0
for s in d["servers"]:
try:
with SSHManager(s["host"], s.get("ssh_port", 22), s["username"],
password=s.get("password") or None,
private_key=s.get("private_key") or None,
connect_timeout=8) as ssh:
out, err, _ = ssh.run_command(PROBE, timeout=60)
строки = [x.strip() for x in (out or "").splitlines() if x.strip()]
if not строки:
print(" ? %-26s нет поднятых интерфейсов" % s["name"][:26])
continue
for стр in строки:
беда = False
if "live=" in стр and "файл=" in стр:
l = int(стр.split("live=")[1].split()[0])
f = int(стр.split("файл=")[1].split()[0])
беда = (l != f)
плохо += 1 if беда else 0
print(" %s %-26s %s" % ("!!" if беда else "ok", s["name"][:26], стр))
except Exception as e:
плохо += 1
print(" !! %-26s НЕ ДОСТУЧАЛСЯ: %s" % (s["name"][:26], str(e)[:60]))
print("\nрасхождений:", плохо)

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Целостность конфигов awg по всему парку: склейки, финальный перевод строки,
проходит ли awg-quick strip и совпадает ли число пиров с живым интерфейсом."""
import json, sys
sys.path.insert(0, "/app")
from managers.ssh_manager import SSHManager
PROBE = r"""
for c in amnezia-awg2 amnezia-awg amnezia-awg31 amnezia-awg3 amnezia-awg-legacy; do
docker ps --format '{{.Names}}' 2>/dev/null | grep -qx $c || continue
for f in awg0.conf wg0.conf; do
p=/opt/amnezia/awg/$f
docker exec $c test -f $p 2>/dev/null || continue
glue=$(docker exec $c grep -c '/32\[Peer\]' $p 2>/dev/null | tr -d '\n')
end=$(docker exec $c sh -c "tail -c 1 $p | od -An -c | tr -d ' \n'")
peers=$(docker exec $c grep -c '^\[Peer\]' $p 2>/dev/null | tr -d '\n')
mode=$(docker exec $c stat -c %a $p 2>/dev/null | tr -d '\n')
bin=wg; iface=wg0
case $f in awg0.conf) bin=awg; iface=awg0;; esac
if docker exec $c sh -c "$bin-quick strip $p > /dev/null 2>&1"; then strip=ok; else strip=СБОЙ; fi
live=$(docker exec $c sh -c "$bin show $iface peers 2>/dev/null | grep -c ." | tr -d '\n')
echo "$c/$f peers=$peers live=$live glue=$glue end=$end mode=$mode strip=$strip"
done
done
"""
d = json.load(open("/app/data/data.json"))
беда = 0
for s in d["servers"]:
try:
with SSHManager(s["host"], s.get("ssh_port", 22), s["username"],
password=s.get("password") or None,
private_key=s.get("private_key") or None,
connect_timeout=8) as ssh:
out, err, _ = ssh.run_command(PROBE, timeout=60)
строки = [x.strip() for x in (out or "").splitlines() if x.strip()]
if not строки:
print(" ? %-26s нет контейнеров awg (%s)" % (s["name"][:26], (err or "")[:40]))
continue
for стр in строки:
плохо = ("glue=0" not in стр) or ("strip=ok" not in стр) or ("end=\\n" not in стр)
if "peers=" in стр and "live=" in стр:
try:
p = int(стр.split("peers=")[1].split()[0])
l = int(стр.split("live=")[1].split()[0])
if p != l:
плохо = True
стр += " ← файл и интерфейс расходятся"
except Exception:
pass
беда += 1 if плохо else 0
print(" %s %-26s %s" % ("!!" if плохо else "ok", s["name"][:26], стр))
except Exception as e:
беда += 1
print(" !! %-26s НЕ ДОСТУЧАЛСЯ: %s" % (s["name"][:26], str(e)[:60]))
print("\nпроблемных строк:", беда)

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Ищем склеенные строки в конфигах awg на всех серверах панели.
Имена переменных в пробе латиницей: sh внутри контейнеров кириллицу не берёт.
"""
import json, sys
sys.path.insert(0, "/app")
from managers.ssh_manager import SSHManager
ПРОБА = (
"for c in amnezia-awg2 amnezia-awg amnezia-awg31 amnezia-awg3 amnezia-awg-legacy; do "
" docker ps --format '{{.Names}}' 2>/dev/null | grep -qx $c || continue; "
" for f in awg0.conf wg0.conf; do "
" p=/opt/amnezia/awg/$f; "
" docker exec $c test -f $p 2>/dev/null || continue; "
" glue=$(docker exec $c grep -c '/32\\[Peer\\]' $p 2>/dev/null || echo 0); "
" endnl=$(docker exec $c sh -c \"tail -c 1 $p | od -An -c | tr -d ' '\" 2>/dev/null); "
" peers=$(docker exec $c grep -c '^\\[Peer\\]' $p 2>/dev/null || echo 0); "
" echo \"$c/$f peers=$peers glue=$glue end=$endnl\"; "
" done; done"
)
d = json.load(open("/app/data/data.json"))
for s in d["servers"]:
try:
with SSHManager(s["host"], s.get("ssh_port", 22), s["username"],
password=s.get("password") or None,
private_key=s.get("private_key") or None,
connect_timeout=8) as ssh:
out, err, _ = ssh.run_command(ПРОБА, timeout=40)
for строка in (out or err).splitlines():
строка = строка.strip()
if not строка:
continue
плохо = ("glue=0" not in строка) or ("end=\\n" not in строка)
print("%s %-26s %s" % (" !!" if плохо else " ", s["name"][:26], строка))
except Exception as e:
print(" %-26s СБОЙ: %s" % (s["name"][:26], str(e)[:50]))

View File

@@ -0,0 +1,30 @@
"""Не осталось ли от проверок тестовых клиентов в clientsTable и в data.json."""
import json, sys
sys.path.insert(0, "/app")
from managers.ssh_manager import SSHManager
МЕТКА = "__проверка_связи"
d = json.load(open("/app/data/data.json"))
в_панели = [c for c in json.dumps(d, ensure_ascii=False).split('"') if МЕТКА in c]
print("следов в data.json панели:", len(в_панели))
ПРОБА = (
"for c in $(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'amnezia-(awg|wireguard|xray)'); do "
" n=$(docker exec $c sh -c \"grep -o '%s[^\\\"]*' /opt/amnezia/awg/clientsTable /opt/amnezia/xray/clientsTable 2>/dev/null | wc -l\"); "
" [ \"$n\" != \"0\" ] && echo \"$c: $n\"; done; echo готово" % МЕТКА
)
всего = 0
for s in d["servers"]:
try:
with SSHManager(s["host"], s.get("ssh_port", 22), s["username"],
password=s.get("password") or None,
private_key=s.get("private_key") or None, connect_timeout=8) as ssh:
o, _, _ = ssh.run_command(ПРОБА, timeout=40)
строки = [x for x in (o or "").splitlines() if x.strip() and "готово" not in x]
if строки:
всего += len(строки)
print(" !!", s["name"], строки)
except Exception as e:
print(" ?", s["name"][:26], str(e)[:40])
print("серверов с остатками:", всего)

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Боевая проверка: на каждом сервере и каждом протоколе создаём тестового
клиента и тут же удаляем. Именно так работает кнопка «Добавить подключение».
Считаем пиров до и после: если после удаления число не вернулось — значит
за нами остался мусор, и это надо видеть.
"""
import json, sys, time
sys.path.insert(0, "/app")
from managers.ssh_manager import SSHManager
from managers.awg_manager import AWGManager
ИМЯ = "__проверка_связи_" + time.strftime("%d%m_%H%M")
ПРОТОКОЛЫ = ("awg", "awg2", "awg3", "awg31", "wireguard")
d = json.load(open("/app/data/data.json"))
итог = {"ok": 0, "сбой": 0, "мусор": 0}
for s in d["servers"]:
протоколы = [p for p in ПРОТОКОЛЫ
if (s.get("protocols", {}).get(p) or {}).get("installed")]
if not протоколы:
print(" – %-26s протоколов AWG не заведено" % s["name"][:26])
continue
for proto in протоколы:
метка = "%s / %s" % (s["name"][:22], proto)
try:
ssh = SSHManager(s["host"], s.get("ssh_port", 22), s["username"],
password=s.get("password") or None,
private_key=s.get("private_key") or None,
connect_timeout=10, connect_retries=1)
ssh.connect()
try:
m = AWGManager(ssh)
было = len(m._get_used_ips(proto))
порт = (s["protocols"][proto] or {}).get("port", "55424")
r = m.add_client(proto, ИМЯ, s["host"], порт)
cid = r.get("client_id") or r.get("clientId")
есть_конфиг = bool(r.get("config"))
стало = len(m._get_used_ips(proto))
m.remove_client(proto, cid)
после = len(m._get_used_ips(proto))
finally:
ssh.disconnect()
если_чисто = (после == было)
итог["ok" if (есть_конфиг and стало == было + 1) else "сбой"] += 1
if not если_чисто:
итог["мусор"] += 1
print(" %s %-30s пиров %d → %d → %d конфиг=%s" % (
"ok" if (есть_конфиг and стало == было + 1 and если_чисто) else "!!",
метка, было, стало, после, "есть" if есть_конфиг else "НЕТ"))
except Exception as e:
итог["сбой"] += 1
print(" !! %-30s %s" % (метка, str(e)[:70]))
print("\nсоздалось и убралось начисто: %d, сбоев: %d, осталось мусора: %d"
% (итог["ok"], итог["сбой"], итог["мусор"]))

87
scripts/clients-meta.mjs Normal file
View File

@@ -0,0 +1,87 @@
/**
* Страховка от затирания clientsTable приложением AmneziaVPN.
*
* Приложение при переподключении/перезагрузке сервера пересобирает clientsTable
* из awg0.conf: остаются только clientId и дефолтные имена «Client N», а имена,
* даты создания и last_config, которые ведёт панель, пропадают.
*
* Панель держит свою копию этих полей в DATA_DIR (том /data переживает
* пересоздание контейнера) и возвращает их на место при следующем чтении.
*/
import fs from "fs";
import path from "path";
/** Поля, которые панель сама никогда не стирает: пустое значение = потеря. */
export const STABLE_FIELDS = ["clientName", "creationDate", "last_config", "allowedIps"];
/**
* Даты, которые панель штатно снимает («Задать дату», отключение по расписанию).
* Их возвращаем только когда запись действительно затёрли, иначе воскресим
* снятое расписание.
*/
export const DATE_FIELDS = ["lastDisconnectedAt", "scheduledTunnelDisconnectAt"];
export const META_FIELDS = [...STABLE_FIELDS, ...DATE_FIELDS];
/** «Client 17» — дефолтное имя от приложения, оно не ценнее сохранённого. */
export function isPlaceholderName(name) {
return /^Client\s*\d+$/i.test(String(name || "").trim());
}
function isMeaningful(field, value) {
if (value == null || value === "") return false;
return field === "clientName" ? !isPlaceholderName(value) : true;
}
function metaFile(dataDir, profileId) {
const safe = String(profileId || "default").replace(/[^\w.-]/g, "_");
return path.join(dataDir, "clients-meta", `${safe}.json`);
}
export function readClientsMeta(dataDir, profileId) {
try {
const data = JSON.parse(fs.readFileSync(metaFile(dataDir, profileId), "utf8"));
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
} catch {
return {};
}
}
export function writeClientsMeta(dataDir, profileId, meta) {
const file = metaFile(dataDir, profileId);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, `${JSON.stringify(meta, null, 2)}\n`);
}
/**
* Возвращает клиентов с восстановленными полями и число восстановленных значений.
* Заодно обновляет снимок метаданных теми значениями, что сейчас на сервере;
* metaChanged=false — снимок писать не нужно (панель читает состояние часто).
*/
export function healClientsMeta(clients, meta) {
let restored = 0;
const before = JSON.stringify(meta);
const nextMeta = { ...meta };
const healed = clients.map((row) => {
const id = row?.clientId;
if (!id) return row;
const userData = { ...(row.userData || {}) };
const saved = nextMeta[id] || {};
// Приложение сносит creationDate целиком — по нему и опознаём затирание.
const wiped = !isMeaningful("creationDate", userData.creationDate) && isMeaningful("creationDate", saved.creationDate);
for (const field of wiped ? META_FIELDS : STABLE_FIELDS) {
if (!isMeaningful(field, userData[field]) && isMeaningful(field, saved[field])) {
userData[field] = saved[field];
restored++;
}
}
const keep = {};
for (const field of META_FIELDS) {
if (isMeaningful(field, userData[field])) keep[field] = userData[field];
}
if (Object.keys(keep).length) nextMeta[id] = keep;
else delete nextMeta[id];
return { ...row, userData };
});
return { clients: healed, restored, meta: nextMeta, metaChanged: JSON.stringify(nextMeta) !== before };
}

View File

@@ -2,7 +2,8 @@
# Установка Amnezia Admin WebUI одной командой (см. README).
set -euo pipefail
GITHUB_REPO="${GITHUB_REPO:-andrey271192/amnezia_web-PRO}"
GITHUB_REPO="${GITHUB_REPO:-PCA/amnezia_web-PRO}"
GIT_BASE="${GIT_BASE:-https://git.bobyrev.com}"
BRANCH="${BRANCH:-main}"
INSTALL_DIR="${INSTALL_DIR:-/opt/amnezia-admin}"
DATA_DIR="${DATA_DIR:-/opt/amnezia-admin-data}"
@@ -21,8 +22,9 @@ need_root() {
need_docker() {
command -v docker >/dev/null 2>&1 || {
echo "Ошибка: нужен Docker."
exit 1
echo "→ Docker не найден — ставлю официальным скриптом get.docker.com..."
curl -fsSL https://get.docker.com | sh || { echo "Ошибка: не удалось поставить Docker."; exit 1; }
systemctl enable --now docker 2>/dev/null || true
}
docker info >/dev/null 2>&1 || {
echo "Ошибка: демон Docker не отвечает."
@@ -40,61 +42,111 @@ 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 с ${GIT_BASE} (вывода может не быть 1–10 мин.; при блокировках задайте зеркало GITHUB_REPO_URL_OVERRIDE или см. CURL_MAX_TIME ниже)."
TMP=$(mktemp -d)
curl -fsSL \
-H 'Cache-Control: no-cache' \
-H 'Pragma: no-cache' \
"https://github.com/${GITHUB_REPO}/archive/refs/heads/${BRANCH}.tar.gz" \
| tar xz -C "${TMP}"
CURL_OPTS=(
-fsSL
-H 'Cache-Control: no-cache'
-H 'Pragma: no-cache'
--connect-timeout "${CURL_CONNECT_TIMEOUT:-15}"
--max-time "${CURL_MAX_TIME:-900}"
--retry "${CURL_RETRY:-2}"
--retry-delay "${CURL_RETRY_DELAY:-5}"
--retry-connrefused
)
if [[ "${INSTALL_SCRIPT_VERBOSE:-}" == "1" ]]; then CURL_OPTS+=(--progress-bar); fi
# ponytail: GitHub-аккаунт заблокирован (10.2026), код живёт на git.bobyrev.com.
CURL_URL="${GITHUB_REPO_URL_OVERRIDE:-${GIT_BASE}/${GITHUB_REPO}/archive/${BRANCH}.tar.gz}"
if ! curl "${CURL_OPTS[@]}" "${CURL_URL}" | tar xz -C "${TMP}"; then
echo "Ошибка: не удалось скачать или распаковать архив (${CURL_URL})."
echo "Подсказка: проверьте доступ к git.bobyrev.com, при необходимости export GITHUB_REPO_URL_OVERRIDE='…' или INSTALL_SCRIPT_VERBOSE=1."
exit 1
fi
echo "→ Перенос распакованного дерева в ${INSTALL_DIR}…"
# API-tarball распаковывается в каталог вида `<owner>-<repo>-<sha7>`,
# archive/refs/heads — в `<repo>-<branch>`. Поэтому берём первый каталог TMP.
__extracted_dir="$(find "${TMP}" -mindepth 1 -maxdepth 1 -type d | head -n1)"
if [[ -z "${__extracted_dir}" ]]; 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 "${TMP}/${REPO_SLUG}-${BRANCH}" "${INSTALL_DIR}"
mv "${__extracted_dir}" "${INSTALL_DIR}"
TMP=""
echo "→ Источники на месте."
fi
mkdir -p "${DATA_DIR}"
# При повторном запуске не менять внешний порт панели, если не указали HOST_PORT явно (по умолчанию 8080).
PREV_HOST_PORT=""
if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
PREV_HOST_PORT="$(docker port "${CONTAINER_NAME}" 3980/tcp 2>/dev/null | head -1 | awk -F: '{print $NF}')"
if [[ -n "${PREV_HOST_PORT}" && "${HOST_PORT}" == "8080" ]]; then
HOST_PORT="${PREV_HOST_PORT}"
echo "→ Уже запущен ${CONTAINER_NAME}: сохраняю внешний порт ${HOST_PORT} (укажите HOST_PORT=… чтобы сменить)."
fi
fi
mkdir -p /opt/amnezia-instances
BOOT_PW=""
PASS_FILE="/root/amnezia-admin.initial-password"
if [[ -f "${DATA_DIR}/password.hash" ]]; then
echo "→ В ${DATA_DIR} уже есть password.hash — контейнер поднимется с прежним паролем."
elif [[ -n "${ADMIN_PASSWORD:-}" ]]; then
if [[ -n "${ADMIN_PASSWORD:-}" ]]; then
BOOT_PW="${ADMIN_PASSWORD}"
if [[ -f "${DATA_DIR}/password.hash" ]]; then
__pw_backup="${DATA_DIR}/password.hash.bak.$(date -u +%Y%m%dT%H%M%SZ)"
cp -p "${DATA_DIR}/password.hash" "${__pw_backup}" 2>/dev/null || true
rm -f "${DATA_DIR}/password.hash"
rm -f "${DATA_DIR}/session.secret"
echo "→ ADMIN_PASSWORD задан — сбрасываю старый пароль панели (backup: ${__pw_backup})."
else
echo "→ Использую ADMIN_PASSWORD из окружения."
elif [[ "${ALLOW_DEFAULT_PASSWORD:-}" == "1" ]] || [[ "${ALLOW_DEFAULT_PASSWORD:-}" == "true" ]]; then
echo "→ ALLOW_DEFAULT_PASSWORD=1 — см. README, пароль по умолчанию для входа."
fi
elif [[ -f "${DATA_DIR}/password.hash" ]]; then
echo "→ В ${DATA_DIR} уже есть password.hash — контейнер поднимется с прежним паролем."
else
BOOT_PW="$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 22 || openssl rand -hex 16)"
BOOT_PW="admin"
umask 077
printf '%s\n' "${BOOT_PW}" >"${PASS_FILE}"
echo "→ Первый пароль записан в ${PASS_FILE}"
echo "→ Первый вход: admin / admin. Смените пароль сразу после входа."
fi
# AWG_PROFILES: не терять при апдейте без переменной (пропадает список «Инстанс»).
AWG_PROFILE_SNAPSHOT="/root/amnezia-admin.awg-profiles.json"
AWG_PROFILES_EXPLICIT=0
if [[ -n "${AWG_PROFILES:-}" ]]; then
AWG_PROFILES_EXPLICIT=1
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}."
@@ -109,20 +161,45 @@ if [[ -z "${AWG_PROFILES:-}" ]] && [[ -f "${AWG_PROFILE_SNAPSHOT}" ]]; then
fi
fi
if [[ -z "${AWG_PROFILES:-}" ]]; then
__awg_multi_count="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E '^amnezia-awg' | wc -l | 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} и снова запустите этот установщик."
# Контейнеры, развёрнутые самой панелью (/opt/amnezia-instances/<имя>), панель знает сама — в AWG_PROFILES их не кладём.
__awg_names="$(docker ps --format '{{.Names}}' 2>/dev/null | awk '/^amnezia-awg/ { print }' | while read -r __n; do [[ -d "/opt/amnezia-instances/${__n}" ]] || echo "${__n}"; done | sort || true)"
__awg_multi_count="$(printf '%s\n' "${__awg_names}" | sed '/^$/d' | wc -l | tr -d '[:space:]')"
__profile_container_count=0
if [[ -n "${AWG_PROFILES:-}" ]]; then
__profile_container_count="$(printf '%s' "${AWG_PROFILES}" | grep -o '"container"' | wc -l | tr -d '[:space:]' || true)"
fi
if [[ "${__awg_multi_count:-0}" =~ ^[0-9]+$ ]] && [[ "${__awg_multi_count}" -eq 1 ]] && [[ -z "${AWG_CONTAINER:-}" ]] && [[ -z "${AWG_PROFILES:-}" ]]; 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
if [[ -z "${AWG_PROFILES:-}" || ( "${AWG_PROFILES_EXPLICIT}" == "0" && "${__profile_container_count:-0}" -lt 2 ) ]]; then
if [[ -n "${AWG_PROFILES:-}" && "${__profile_container_count:-0}" -lt 2 ]]; then
echo "→ Найден старый одиночный AWG_PROFILES, но контейнеров amnezia-awg*: ${__awg_multi_count}. Пересобираю профили автоматически."
fi
__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 WARP_SSH_INSTALL_DIR; do
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=""
while IFS= read -r __line; do
@@ -150,7 +227,7 @@ docker build "${DOCKER_BUILD_EXTRA[@]}" -t amnezia-admin:latest "${INSTALL_DIR}"
docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
RUN_ENV=(
-e AWG_CONTAINER="${AWG_CONTAINER:-amnezia-awg2}"
-e AWG_CONTAINER="${AWG_CONTAINER:-amnezia-awg}"
)
if [[ -n "${AWG_PROFILES:-}" ]]; then
@@ -175,45 +252,45 @@ for __warp_var in WARP_DIR WARP_CONF_PATH WARP_CLIENTS_LIST AMNEZIA_START_SCRIPT
fi
done
if [[ -z "${CLIENT_CONFIG_ENDPOINT:-}" ]]; then
__pub_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
if [[ -n "${__pub_ip}" ]]; then
CLIENT_CONFIG_ENDPOINT="${__pub_ip}"
echo "→ CLIENT_CONFIG_ENDPOINT авто: ${CLIENT_CONFIG_ENDPOINT} (можно переопределить переменной окружения)"
fi
fi
for __export_var in CLIENT_CONFIG_ENDPOINT CLIENT_EXPORT_DNS1 CLIENT_EXPORT_DNS2 EXPORT_CONFIG_SECRET; do
if [[ -n "${!__export_var:-}" ]]; then
RUN_ENV+=( -e "${__export_var}=${!__export_var}" )
fi
done
for __ui_var in UI_HIDE_SECTIONS UI_HIDE_USERS UI_HIDE_WARP UI_HIDE_CASCADE WARP_SSH_INSTALL_DIR; do
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 [[ -n "${!__ui_var:-}" ]]; then
RUN_ENV+=( -e "${__ui_var}=${!__ui_var}" )
fi
done
if [[ -n "${BOOT_PW}" ]]; then
RUN_ENV+=( -e "ADMIN_PASSWORD=${BOOT_PW}" )
elif [[ "${ALLOW_DEFAULT_PASSWORD:-}" == "1" ]] || [[ "${ALLOW_DEFAULT_PASSWORD:-}" == "true" ]]; then
RUN_ENV+=( -e "ALLOW_DEFAULT_PASSWORD=1" )
fi
if [[ -n "${AMNEZIA_EDITION:-}" ]]; then
RUN_ENV+=( -e "AMNEZIA_EDITION=${AMNEZIA_EDITION}" )
elif [[ -f "${INSTALL_DIR}/.amnezia-panel-edition" ]]; then
__PE="$(tr -d '\r\n' <"${INSTALL_DIR}/.amnezia-panel-edition" | head -c 48)"
if [[ -n "${__PE}" ]]; then
RUN_ENV+=( -e "AMNEZIA_EDITION=${__PE}" )
echo "→ AMNEZIA_EDITION из ${INSTALL_DIR}/.amnezia-panel-edition: ${__PE}"
fi
fi
for __ce_var in COMMUNITY_UPGRADE_URL COMMUNITY_UPGRADE_PITCH; do
if [[ -n "${!__ce_var:-}" ]]; then
RUN_ENV+=( -e "${__ce_var}=${!__ce_var}" )
for __mt_vars in MTPRO_PROXY_CONTAINER MTPRO_PROXY_IMAGE MTPRO_INTERNAL_PORT MTPRO_PUBLISH_PORT \
MTPRO_PUBLISH_BIND MTPRO_PUBLIC_HOST; do
if [[ -n "${!__mt_vars:-}" ]]; then
RUN_ENV+=( -e "${__mt_vars}=${!__mt_vars}" )
fi
done
if [[ -n "${BOOT_PW}" ]]; then
RUN_ENV+=( -e "ADMIN_PASSWORD=${BOOT_PW}" )
fi
IP="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
docker run -d --name "${CONTAINER_NAME}" --restart unless-stopped \
-p "${HOST_PORT}:3980" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "${DATA_DIR}:/data" \
-v /opt/amnezia-instances:/opt/amnezia-instances \
"${RUN_ENV[@]}" \
amnezia-admin:latest
@@ -230,9 +307,43 @@ if [[ "${SKIP_LANDING:-}" != "1" ]] && [[ -d "${INSTALL_DIR}/landing" ]]; then
echo "⚠ Не удалось запустить лендинг (часто порт ${LANDING_PORT} занят). Поставьте LANDING_PORT=8081 или SKIP_LANDING=1."
fi
else
# ponytail: старый контейнер переживает переустановку (--restart unless-stopped)
# и продолжает держать LANDING_PORT — снимаем его, иначе SKIP_LANDING=1 не даёт эффекта
if docker rm -f "${LANDING_CONTAINER}" >/dev/null 2>&1; then
echo "→ Старый лендинг ${LANDING_CONTAINER} остановлен и удалён."
fi
echo "→ Лендинг пропущен (SKIP_LANDING=1 или нет каталога landing)."
fi
# ── Диагностика AmneziaWG: что увидит панель ──────────────────────────────
echo ""
echo "── Проверка AmneziaWG ──"
__awg_running="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E '^amnezia-?awg|amnezia.*wg|awg' || true)"
__awg_with_conf=""
for __c in $(docker ps --format '{{.Names}}' 2>/dev/null); do
if docker exec "${__c}" sh -c 'test -f /opt/amnezia/awg/awg0.conf || test -f /opt/amnezia/awg/wg0.conf' 2>/dev/null; then
__awg_with_conf="${__awg_with_conf} ${__c}"
fi
done
__awg_with_conf="$(echo "${__awg_with_conf}" | xargs 2>/dev/null || true)"
if [[ -n "${__awg_with_conf}" ]]; then
echo "✓ Найдены контейнеры AmneziaWG: ${__awg_with_conf}"
echo " Панель подхватит их автоматически (рантайм-обнаружение) — ручной AWG_PROFILES не нужен."
for __c in ${__awg_with_conf}; do
__cnt="$(docker exec "${__c}" sh -c 'cat /opt/amnezia/awg/clientsTable 2>/dev/null' 2>/dev/null | grep -c clientId || true)"
__prt="$(docker exec "${__c}" sh -c 'grep -h ListenPort /opt/amnezia/awg/*.conf 2>/dev/null' 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)"
echo " • ${__c}: порт ${__prt:-?}/udp, клиентов в таблице: ${__cnt:-0}"
done
else
echo "⚠ Не найдено ни одного контейнера AmneziaWG (нет /opt/amnezia/awg/*.conf ни в одном контейнере)."
echo " Это НОРМАЛЬНО для чистого VPS. Дальше есть два пути:"
echo " 1) Развернуть AmneziaWG прямо из панели: раздел «Протоколы / инстансы» → выбрать вариант и порт → «Развернуть инстанс»."
echo " 2) Поставить сервер приложением Amnezia на этот VPS — панель подхватит контейнер сама."
echo " Пока сервера нет, список клиентов будет пустым и создание клиента вернёт ошибку «контейнер не найден» — это ожидаемо."
fi
echo "────────────────────────"
echo ""
echo "=== Готово ==="
echo "Админ-панель: http://${IP:-SERVER_IP}:${HOST_PORT}"
@@ -242,8 +353,5 @@ fi
if [[ -f "${PASS_FILE}" ]]; then
echo "Первый пароль: $(cat "${PASS_FILE}")"
fi
if [[ "${ALLOW_DEFAULT_PASSWORD:-}" == "1" ]] || [[ "${ALLOW_DEFAULT_PASSWORD:-}" == "true" ]]; then
echo "Пароль по умолчанию (смените в панели): AmneziaAdmin!ChangeMe"
fi
echo ""
echo "Удаление: curl -fsSL https://raw.githubusercontent.com/${GITHUB_REPO}/${BRANCH}/scripts/uninstall.sh | sudo bash"
echo "Удаление: curl -fsSL ${GIT_BASE}/${GITHUB_REPO}/raw/branch/${BRANCH}/scripts/uninstall.sh | sudo bash"

View File

@@ -0,0 +1,60 @@
// node scripts/test-clients-meta.mjs — самопроверка страховки clientsTable
import assert from "assert";
import { healClientsMeta, isPlaceholderName } from "./clients-meta.mjs";
const good = [
{
clientId: "A=",
userData: {
clientName: "Mama",
creationDate: "Sat Jun 27 2026",
last_config: "{...}",
lastDisconnectedAt: "2027-05-06T10:00:00.000Z",
},
},
{ clientId: "B=", userData: { clientName: "teplici", creationDate: "Sat Jul 04 2026" } },
];
// 1. Снимок берёт живые значения.
const snap = healClientsMeta(good, {});
assert.equal(snap.restored, 0);
assert.equal(snap.meta["A="].clientName, "Mama");
assert.equal(snap.metaChanged, true);
// 2. Приложение затёрло таблицу — имена и даты возвращаются, новый клиент не трогается.
const wiped = [
{ clientId: "A=", userData: { clientName: "Client 0" } },
{ clientId: "B=", userData: { clientName: "Client 1" } },
{ clientId: "C=", userData: { clientName: "Client 2" } },
];
const fixed = healClientsMeta(wiped, snap.meta);
assert.equal(fixed.clients[0].userData.clientName, "Mama");
assert.equal(fixed.clients[0].userData.last_config, "{...}");
assert.equal(fixed.clients[0].userData.lastDisconnectedAt, "2027-05-06T10:00:00.000Z");
assert.equal(fixed.clients[1].userData.creationDate, "Sat Jul 04 2026");
assert.equal(fixed.clients[2].userData.clientName, "Client 2");
assert.equal(fixed.restored, 6);
// 2б. Дату сняли в панели (запись целая) — снимок её не воскрешает и забывает.
const cleared = healClientsMeta(
[{ clientId: "A=", userData: { clientName: "Mama", creationDate: "Sat Jun 27 2026", last_config: "{...}" } }],
snap.meta
);
assert.equal(cleared.clients[0].userData.lastDisconnectedAt, undefined);
assert.equal(cleared.meta["A="].lastDisconnectedAt, undefined);
// 3. Повторный прогон уже целой таблицы ничего не чинит и не переписывает снимок.
const again = healClientsMeta(fixed.clients, fixed.meta);
assert.equal(again.restored, 0);
assert.equal(again.metaChanged, false);
// 4. Переименование в панели побеждает старый снимок.
const renamed = healClientsMeta(
[{ clientId: "A=", userData: { clientName: "Мама новая" } }],
snap.meta
);
assert.equal(renamed.clients[0].userData.clientName, "Мама новая");
assert.equal(renamed.meta["A="].clientName, "Мама новая");
assert.ok(isPlaceholderName("Client 17") && !isPlaceholderName("Vorot2"));
console.log("clients-meta: ok");

2027
server.js

File diff suppressed because it is too large Load Diff