GUI: migrate storage from PostgreSQL to SQLite and expose conf download

This commit is contained in:
Ruslan
2026-04-14 12:08:10 +03:00
parent 39d17534e0
commit cd5ba53802
5 changed files with 31 additions and 79 deletions

View File

@@ -8,7 +8,7 @@
- Быстро развернуть WireGuard-сервер (`wg-quick@wg0`) с автозапуском через `systemd`. - Быстро развернуть WireGuard-сервер (`wg-quick@wg0`) с автозапуском через `systemd`.
- Включить IP forwarding и NAT для выхода клиентов в интернет через сервер. - Включить IP forwarding и NAT для выхода клиентов в интернет через сервер.
- Установить встроенный GUI для управления peer'ами и QR (`wg-admin-gui`) с хранением данных в PostgreSQL. - Установить встроенный GUI для управления peer'ами и QR (`wg-admin-gui`) с хранением данных в SQLite.
- Автоматизировать добавление клиента с клиентской машины через SSH на сервер. - Автоматизировать добавление клиента с клиентской машины через SSH на сервер.
- Поддержать 2 режима маршрутизации клиента: - Поддержать 2 режима маршрутизации клиента:
- полный туннель (весь трафик через VPN) - полный туннель (весь трафик через VPN)
@@ -36,7 +36,7 @@
- Сервер разворачивается нативно на `wg-quick` + `systemd` (стабильность после reboot). - Сервер разворачивается нативно на `wg-quick` + `systemd` (стабильность после reboot).
- GUI (`wg-admin-gui`) работает поверх того же `/etc/wireguard`, где лежит серверный конфиг. - GUI (`wg-admin-gui`) работает поверх того же `/etc/wireguard`, где лежит серверный конфиг.
- Метаданные GUI хранятся в PostgreSQL. - Метаданные GUI хранятся в SQLite.
- На сервере ставится `wg-syncconf@wg0.path`: при изменении `/etc/wireguard/wg0.conf` конфиг автоматически применяется в живой интерфейс `wg0`. - На сервере ставится `wg-syncconf@wg0.path`: при изменении `/etc/wireguard/wg0.conf` конфиг автоматически применяется в живой интерфейс `wg0`.
- Клиентский скрипт: - Клиентский скрипт:
1. генерирует ключи локально, 1. генерирует ключи локально,
@@ -60,7 +60,6 @@
- `wireguard`, `wireguard-tools` - `wireguard`, `wireguard-tools`
- `iproute2`, `iptables` - `iproute2`, `iptables`
- `curl`, `ca-certificates`, `openssl`, `qrencode` - `curl`, `ca-certificates`, `openssl`, `qrencode`
- `docker.io` и один из пакетов: `docker-compose-plugin` или `docker-compose` (для PostgreSQL контейнера GUI)
- `python3`, `python3-venv`, `python3-pip` (для `wg-admin-gui`) - `python3`, `python3-venv`, `python3-pip` (для `wg-admin-gui`)
### Клиент ### Клиент
@@ -79,7 +78,7 @@
sudo bash server/install_server.sh sudo bash server/install_server.sh
``` ```
Важно: серверный установщик теперь всегда выполняет полный reset прошлого состояния (`/etc/wireguard` + данные GUI/PostgreSQL) и поднимает всё заново. Важно: серверный установщик теперь всегда выполняет полный reset прошлого состояния (`/etc/wireguard` + данные GUI/SQLite) и поднимает всё заново.
### Запуск сервера одной командой (без `git clone`) ### Запуск сервера одной командой (без `git clone`)
@@ -234,7 +233,7 @@ http://203.0.113.10:5000
1. Откройте GUI по ссылке из итоговой сводки установки. 1. Откройте GUI по ссылке из итоговой сводки установки.
2. Перейдите в раздел добавления peer. 2. Перейдите в раздел добавления peer.
3. Создайте клиента. 3. Создайте клиента.
4. Используйте показанный QR. 4. Используйте показанный QR или скачайте готовый `.conf`.
5. На iPhone: WireGuard → `Add Tunnel``Create from QR code` и отсканируйте код. 5. На iPhone: WireGuard → `Add Tunnel``Create from QR code` и отсканируйте код.
## Взаимодействие клиента с сервером ## Взаимодействие клиента с сервером
@@ -309,7 +308,6 @@ ls -l /usr/local/sbin/wg-peerctl
4. GUI недоступен: 4. GUI недоступен:
```bash ```bash
sudo systemctl status wg-admin-gui --no-pager sudo systemctl status wg-admin-gui --no-pager
sudo docker ps | grep wg-admin-postgres
sudo ss -tulpn | grep 5000 sudo ss -tulpn | grep 5000
``` ```

View File

@@ -2,18 +2,17 @@
import base64 import base64
import io import io
import os import os
import sqlite3
import subprocess import subprocess
from datetime import datetime from datetime import datetime
import qrcode import qrcode
from flask import Flask, redirect, render_template, request, url_for, flash, Response from flask import Flask, redirect, render_template, request, url_for, flash, Response
from psycopg import connect
from psycopg.rows import dict_row
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get("APP_SECRET", "dev-secret") app.secret_key = os.environ.get("APP_SECRET", "dev-secret")
DB_DSN = os.environ.get("DB_DSN", "postgresql://wgadmin:wgadmin@127.0.0.1:5432/wgadmin") DB_PATH = os.environ.get("DB_PATH", "/opt/wg-admin-gui/data/wgadmin.db")
WG_INTERFACE = os.environ.get("WG_INTERFACE", "wg0") WG_INTERFACE = os.environ.get("WG_INTERFACE", "wg0")
WG_META_FILE = os.environ.get("WG_META_FILE", "/etc/wireguard/wg-meta.env") WG_META_FILE = os.environ.get("WG_META_FILE", "/etc/wireguard/wg-meta.env")
ADMIN_USER = os.environ.get("ADMIN_USER", "admin") ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
@@ -21,23 +20,25 @@ ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "")
def db_conn(): def db_conn():
return connect(DB_DSN, row_factory=dict_row) os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def ensure_schema(): def ensure_schema():
with db_conn() as conn, conn.cursor() as cur: with db_conn() as conn:
cur.execute( cur = conn.cursor()
""" cur.execute("""
CREATE TABLE IF NOT EXISTS peers ( CREATE TABLE IF NOT EXISTS peers (
id BIGSERIAL PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
public_key TEXT UNIQUE NOT NULL, public_key TEXT UNIQUE NOT NULL,
client_address TEXT, client_address TEXT,
advertised_routes TEXT, advertised_routes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now() created_at TEXT NOT NULL DEFAULT (datetime('now'))
); );
""" """)
)
conn.commit() conn.commit()
@@ -150,9 +151,10 @@ def _schema():
def index(): def index():
meta = load_meta() meta = load_meta()
runtime = {p["public_key"]: p for p in wg_dump()} runtime = {p["public_key"]: p for p in wg_dump()}
with db_conn() as conn, conn.cursor() as cur: with db_conn() as conn:
cur = conn.cursor()
cur.execute("SELECT * FROM peers ORDER BY id DESC") cur.execute("SELECT * FROM peers ORDER BY id DESC")
db_peers = cur.fetchall() db_peers = [dict(r) for r in cur.fetchall()]
items = [] items = []
seen = set() seen = set()
@@ -260,16 +262,14 @@ def new_peer():
client_conf = "\n".join(conf_lines) client_conf = "\n".join(conf_lines)
qr_b64 = to_png_b64(client_conf) qr_b64 = to_png_b64(client_conf)
with db_conn() as conn, conn.cursor() as cur: with db_conn() as conn:
cur.execute( cur = conn.cursor()
""" cur.execute("""
INSERT INTO peers(name, public_key, client_address, advertised_routes) INSERT INTO peers(name, public_key, client_address, advertised_routes)
VALUES (%s,%s,%s,%s) VALUES (?,?,?,?)
ON CONFLICT(public_key) ON CONFLICT(public_key)
DO UPDATE SET name=excluded.name, client_address=excluded.client_address, advertised_routes=excluded.advertised_routes DO UPDATE SET name=excluded.name, client_address=excluded.client_address, advertised_routes=excluded.advertised_routes
""", """, (name, client_pub, client_addr, routes))
(name, client_pub, client_addr, routes),
)
conn.commit() conn.commit()
return render_template( return render_template(

View File

@@ -1,4 +1,3 @@
Flask==3.0.3 Flask==3.0.3
psycopg[binary]==3.2.1
qrcode==7.4.2 qrcode==7.4.2
gunicorn==23.0.0 gunicorn==23.0.0

View File

@@ -2,6 +2,7 @@
{% block content %} {% block content %}
<h2>Peer создан: {{ name }}</h2> <h2>Peer создан: {{ name }}</h2>
<p>PublicKey: <span class="mono">{{ public_key }}</span></p> <p>PublicKey: <span class="mono">{{ public_key }}</span></p>
<p><a href="data:text/plain;charset=utf-8,{{ client_conf | urlencode }}" download="{{ name }}.conf">Скачать {{ name }}.conf</a></p>
<div class="grid2"> <div class="grid2">
<div> <div>
<h3>QR</h3> <h3>QR</h3>

View File

@@ -24,7 +24,6 @@ GUI_USER="admin"
GUI_PASSWORD="" GUI_PASSWORD=""
GUI_PASSWORD_GENERATED=0 GUI_PASSWORD_GENERATED=0
GUI_RESET_DB="no" GUI_RESET_DB="no"
GUI_DB_PASSWORD=""
usage() { usage() {
cat <<'USAGE' cat <<'USAGE'
@@ -116,16 +115,6 @@ validate_inputs() {
fi fi
} }
detect_compose_cmd() {
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose)
else
die "Не найден docker compose. Установите docker-compose-plugin или docker-compose."
fi
}
reset_existing_install() { reset_existing_install() {
log_warn "Выполняю полный reset предыдущей инсталляции WireGuard/GUI" log_warn "Выполняю полный reset предыдущей инсталляции WireGuard/GUI"
@@ -149,14 +138,9 @@ reset_existing_install() {
rm -f /etc/systemd/system/wg-admin-gui.service rm -f /etc/systemd/system/wg-admin-gui.service
if command -v docker >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1; then
if [[ -f /opt/wg-admin-gui/docker-compose.yml ]]; then
detect_compose_cmd
(cd /opt/wg-admin-gui && "${COMPOSE_CMD[@]}" down --remove-orphans >/dev/null 2>&1) || true
fi
if [[ -f /opt/wireguard-ui/docker-compose.yml ]]; then if [[ -f /opt/wireguard-ui/docker-compose.yml ]]; then
detect_compose_cmd (cd /opt/wireguard-ui && docker compose down --remove-orphans >/dev/null 2>&1) || true
(cd /opt/wireguard-ui && "${COMPOSE_CMD[@]}" down --remove-orphans >/dev/null 2>&1) || true (cd /opt/wireguard-ui && docker-compose down --remove-orphans >/dev/null 2>&1) || true
fi fi
docker rm -f wireguard-ui wg-admin-postgres >/dev/null 2>&1 || true docker rm -f wireguard-ui wg-admin-postgres >/dev/null 2>&1 || true
@@ -213,8 +197,6 @@ collect_inputs() {
fi fi
fi fi
fi fi
GUI_DB_PASSWORD="$(random_alnum 24)"
[[ "$GUI_RESET_DB" == "yes" || "$GUI_RESET_DB" == "no" ]] || die "--gui-reset-db должен быть yes или no" [[ "$GUI_RESET_DB" == "yes" || "$GUI_RESET_DB" == "no" ]] || die "--gui-reset-db должен быть yes или no"
fi fi
@@ -224,15 +206,7 @@ collect_inputs() {
install_packages() { install_packages() {
apt_install_if_missing \ apt_install_if_missing \
wireguard wireguard-tools iproute2 iptables curl ca-certificates openssl \ wireguard wireguard-tools iproute2 iptables curl ca-certificates openssl \
docker.io python3 python3-venv python3-pip python3 python3-venv python3-pip
if apt-cache show docker-compose-plugin >/dev/null 2>&1; then
apt_install_if_missing docker-compose-plugin
elif apt-cache show docker-compose >/dev/null 2>&1; then
apt_install_if_missing docker-compose
else
log_warn "Не найден пакет docker-compose-plugin/docker-compose. Проверьте репозитории APT."
fi
} }
setup_sysctl() { setup_sysctl() {
@@ -371,37 +345,17 @@ EOF_SYNC_PATH
setup_gui() { setup_gui() {
[[ "$GUI_ENABLE" == "yes" ]] || { log_warn "GUI отключен (GUI_ENABLE=no)"; return; } [[ "$GUI_ENABLE" == "yes" ]] || { log_warn "GUI отключен (GUI_ENABLE=no)"; return; }
mkdir -p /opt/wg-admin-gui/{app,pgdata} mkdir -p /opt/wg-admin-gui/{app,data}
safe_chmod_700 /opt/wg-admin-gui safe_chmod_700 /opt/wg-admin-gui
cp -a "${PROJECT_ROOT}/gui/." /opt/wg-admin-gui/app/ cp -a "${PROJECT_ROOT}/gui/." /opt/wg-admin-gui/app/
cat > /opt/wg-admin-gui/docker-compose.yml <<EOF_COMPOSE
services:
postgres:
image: postgres:16-alpine
container_name: wg-admin-postgres
restart: unless-stopped
environment:
- POSTGRES_DB=wgadmin
- POSTGRES_USER=wgadmin
- POSTGRES_PASSWORD=${GUI_DB_PASSWORD}
ports:
- "127.0.0.1:5432:5432"
volumes:
- /opt/wg-admin-gui/pgdata:/var/lib/postgresql/data
EOF_COMPOSE
systemd_enable_now docker.service
detect_compose_cmd
(cd /opt/wg-admin-gui && "${COMPOSE_CMD[@]}" up -d --remove-orphans)
python3 -m venv /opt/wg-admin-gui/venv python3 -m venv /opt/wg-admin-gui/venv
/opt/wg-admin-gui/venv/bin/pip install --upgrade pip >/dev/null /opt/wg-admin-gui/venv/bin/pip install --upgrade pip >/dev/null
/opt/wg-admin-gui/venv/bin/pip install -r /opt/wg-admin-gui/app/requirements.txt >/dev/null /opt/wg-admin-gui/venv/bin/pip install -r /opt/wg-admin-gui/app/requirements.txt >/dev/null
cat > /opt/wg-admin-gui/wg-admin-gui.env <<EOF_ENV cat > /opt/wg-admin-gui/wg-admin-gui.env <<EOF_ENV
DB_DSN=postgresql://wgadmin:${GUI_DB_PASSWORD}@127.0.0.1:5432/wgadmin DB_PATH=/opt/wg-admin-gui/data/wgadmin.db
WG_INTERFACE=${WG_INTERFACE} WG_INTERFACE=${WG_INTERFACE}
WG_META_FILE=/etc/wireguard/wg-meta.env WG_META_FILE=/etc/wireguard/wg-meta.env
ADMIN_USER=${GUI_USER} ADMIN_USER=${GUI_USER}
@@ -414,7 +368,7 @@ EOF_ENV
cat > /etc/systemd/system/wg-admin-gui.service <<EOF_SERVICE cat > /etc/systemd/system/wg-admin-gui.service <<EOF_SERVICE
[Unit] [Unit]
Description=WG Admin GUI Description=WG Admin GUI
After=network-online.target docker.service After=network-online.target
Wants=network-online.target Wants=network-online.target
[Service] [Service]