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).
This commit is contained in:
andrey271192
2026-06-16 21:33:02 +03:00
parent f1b7fa522c
commit 546c1127f8
7 changed files with 505 additions and 13 deletions

View File

@@ -1178,3 +1178,26 @@ tr:last-child td {
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; }