GUI: migrate storage from PostgreSQL to SQLite and expose conf download
This commit is contained in:
10
README.md
10
README.md
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
40
gui/app.py
40
gui/app.py
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user