Compare commits
11 Commits
39d17534e0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
530e93c1df | ||
|
|
02ccad1805 | ||
|
|
22680a0df5 | ||
|
|
8e124be1f0 | ||
|
|
54868b99cd | ||
|
|
69f51bd5d7 | ||
|
|
bb6cdb58ed | ||
|
|
bdfc648bba | ||
|
|
9b31c5d5c5 | ||
|
|
8de590c5d0 | ||
|
|
cd5ba53802 |
11
README.md
11
README.md
@@ -8,7 +8,7 @@
|
||||
|
||||
- Быстро развернуть WireGuard-сервер (`wg-quick@wg0`) с автозапуском через `systemd`.
|
||||
- Включить IP forwarding и NAT для выхода клиентов в интернет через сервер.
|
||||
- Установить встроенный GUI для управления peer'ами и QR (`wg-admin-gui`) с хранением данных в PostgreSQL.
|
||||
- Установить встроенный GUI для управления peer'ами и QR (`wg-admin-gui`) с хранением данных в SQLite.
|
||||
- Автоматизировать добавление клиента с клиентской машины через SSH на сервер.
|
||||
- Поддержать 2 режима маршрутизации клиента:
|
||||
- полный туннель (весь трафик через VPN)
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
- Сервер разворачивается нативно на `wg-quick` + `systemd` (стабильность после reboot).
|
||||
- GUI (`wg-admin-gui`) работает поверх того же `/etc/wireguard`, где лежит серверный конфиг.
|
||||
- Метаданные GUI хранятся в PostgreSQL.
|
||||
- Метаданные GUI хранятся в SQLite.
|
||||
- На сервере ставится `wg-syncconf@wg0.path`: при изменении `/etc/wireguard/wg0.conf` конфиг автоматически применяется в живой интерфейс `wg0`.
|
||||
- Клиентский скрипт:
|
||||
1. генерирует ключи локально,
|
||||
@@ -60,7 +60,6 @@
|
||||
- `wireguard`, `wireguard-tools`
|
||||
- `iproute2`, `iptables`
|
||||
- `curl`, `ca-certificates`, `openssl`, `qrencode`
|
||||
- `docker.io` и один из пакетов: `docker-compose-plugin` или `docker-compose` (для PostgreSQL контейнера GUI)
|
||||
- `python3`, `python3-venv`, `python3-pip` (для `wg-admin-gui`)
|
||||
|
||||
### Клиент
|
||||
@@ -79,7 +78,7 @@
|
||||
sudo bash server/install_server.sh
|
||||
```
|
||||
|
||||
Важно: серверный установщик теперь всегда выполняет полный reset прошлого состояния (`/etc/wireguard` + данные GUI/PostgreSQL) и поднимает всё заново.
|
||||
Важно: серверный установщик теперь всегда выполняет полный reset прошлого состояния (`/etc/wireguard` + данные GUI/SQLite) и поднимает всё заново.
|
||||
|
||||
### Запуск сервера одной командой (без `git clone`)
|
||||
|
||||
@@ -161,6 +160,7 @@ tmp="$(mktemp -d)" && curl -fL "https://git.ruslan.xyz/ruslan/Wireguard_server/a
|
||||
Если за клиентом есть локальная сеть (например `192.168.33.0/24`), передайте `--advertise-subnets 192.168.33.0/24`, чтобы сервер маршрутизировал эту сеть через клиента.
|
||||
Если `--advertise-subnets` не задан, скрипт автоматически пытается определить LAN-сети клиента и объявить их на сервере.
|
||||
В режиме `split`, если `--allowed-ips` не задан, скрипт автоматически использует сеть WG сервера.
|
||||
При объявлении сетей за клиентом скрипт автоматически включает `ip_forward` и добавляет правила `iptables` (forward + nat) через `PostUp/PostDown`.
|
||||
|
||||
### Non-interactive пример (SSH-ключ)
|
||||
|
||||
@@ -234,7 +234,7 @@ http://203.0.113.10:5000
|
||||
1. Откройте GUI по ссылке из итоговой сводки установки.
|
||||
2. Перейдите в раздел добавления peer.
|
||||
3. Создайте клиента.
|
||||
4. Используйте показанный QR.
|
||||
4. Используйте показанный QR или скачайте готовый `.conf`.
|
||||
5. На iPhone: WireGuard → `Add Tunnel` → `Create from QR code` и отсканируйте код.
|
||||
|
||||
## Взаимодействие клиента с сервером
|
||||
@@ -309,7 +309,6 @@ ls -l /usr/local/sbin/wg-peerctl
|
||||
4. GUI недоступен:
|
||||
```bash
|
||||
sudo systemctl status wg-admin-gui --no-pager
|
||||
sudo docker ps | grep wg-admin-postgres
|
||||
sudo ss -tulpn | grep 5000
|
||||
```
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ SSH_PASSWORD=""
|
||||
|
||||
KEEPALIVE="25"
|
||||
CLIENT_ADDRESS_PREFIX="24"
|
||||
FORWARDING_MODE="disabled"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
@@ -303,6 +304,12 @@ build_allowed_ips() {
|
||||
build_route_hooks_if_needed() {
|
||||
PRE_UP=""
|
||||
POST_DOWN=""
|
||||
POST_UP_EXTRA_1=""
|
||||
POST_UP_EXTRA_2=""
|
||||
POST_UP_EXTRA_3=""
|
||||
POST_DOWN_EXTRA_1=""
|
||||
POST_DOWN_EXTRA_2=""
|
||||
POST_DOWN_EXTRA_3=""
|
||||
|
||||
if [[ "$TUNNEL_MODE" != "full" ]]; then
|
||||
return
|
||||
@@ -321,6 +328,41 @@ build_route_hooks_if_needed() {
|
||||
fi
|
||||
}
|
||||
|
||||
detect_iface_for_cidr() {
|
||||
local cidr="$1"
|
||||
ip -4 route show "$cidr" 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="dev") {print $(i+1); exit}}'
|
||||
}
|
||||
|
||||
build_lan_nat_hooks_if_needed() {
|
||||
[[ -n "$ADVERTISE_SUBNETS" ]] || return
|
||||
|
||||
local first_cidr lan_iface wg_net
|
||||
first_cidr="$(echo "$ADVERTISE_SUBNETS" | awk -F',' '{gsub(/ /,"",$1); print $1}')"
|
||||
lan_iface="$(detect_iface_for_cidr "$first_cidr" || true)"
|
||||
[[ -n "$lan_iface" ]] || lan_iface="$(detect_default_iface || true)"
|
||||
[[ -n "$lan_iface" ]] || { log_warn "Не удалось определить LAN-интерфейс для NAT/forwarding."; return; }
|
||||
|
||||
wg_net="${SERVER_WG_NETWORK:-10.66.66.0/24}"
|
||||
|
||||
local fwd="/etc/sysctl.d/99-wireguard-client-forwarding.conf"
|
||||
cat > "$fwd" <<EOF_FWD
|
||||
net.ipv4.ip_forward=1
|
||||
net.ipv4.conf.all.rp_filter=2
|
||||
net.ipv4.conf.default.rp_filter=2
|
||||
EOF_FWD
|
||||
sysctl --system >/dev/null || true
|
||||
|
||||
POST_UP_EXTRA_1="PostUp = iptables -C FORWARD -i ${WG_INTERFACE} -o ${lan_iface} -j ACCEPT 2>/dev/null || iptables -A FORWARD -i ${WG_INTERFACE} -o ${lan_iface} -j ACCEPT"
|
||||
POST_UP_EXTRA_2="PostUp = iptables -C FORWARD -i ${lan_iface} -o ${WG_INTERFACE} -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables -A FORWARD -i ${lan_iface} -o ${WG_INTERFACE} -m state --state RELATED,ESTABLISHED -j ACCEPT"
|
||||
POST_UP_EXTRA_3="PostUp = iptables -t nat -C POSTROUTING -s ${wg_net} -o ${lan_iface} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s ${wg_net} -o ${lan_iface} -j MASQUERADE"
|
||||
|
||||
POST_DOWN_EXTRA_1="PostDown = iptables -D FORWARD -i ${WG_INTERFACE} -o ${lan_iface} -j ACCEPT 2>/dev/null || true"
|
||||
POST_DOWN_EXTRA_2="PostDown = iptables -D FORWARD -i ${lan_iface} -o ${WG_INTERFACE} -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true"
|
||||
POST_DOWN_EXTRA_3="PostDown = iptables -t nat -D POSTROUTING -s ${wg_net} -o ${lan_iface} -j MASQUERADE 2>/dev/null || true"
|
||||
|
||||
FORWARDING_MODE="enabled via ${lan_iface} (wg:${wg_net})"
|
||||
}
|
||||
|
||||
build_client_interface_address() {
|
||||
local ip_only
|
||||
ip_only="${CLIENT_ADDRESS%%/*}"
|
||||
@@ -360,6 +402,14 @@ write_client_config() {
|
||||
if [[ -n "$POST_DOWN" ]]; then
|
||||
echo "$POST_DOWN"
|
||||
fi
|
||||
if [[ -n "$POST_UP_EXTRA_1" ]]; then
|
||||
echo "$POST_UP_EXTRA_1"
|
||||
echo "$POST_UP_EXTRA_2"
|
||||
echo "$POST_UP_EXTRA_3"
|
||||
echo "$POST_DOWN_EXTRA_1"
|
||||
echo "$POST_DOWN_EXTRA_2"
|
||||
echo "$POST_DOWN_EXTRA_3"
|
||||
fi
|
||||
echo
|
||||
echo "[Peer]"
|
||||
echo "PublicKey = ${SERVER_PUBLIC_KEY}"
|
||||
@@ -403,6 +453,7 @@ Endpoint сервера: ${SERVER_ENDPOINT}
|
||||
SSH сервер: ${SERVER_USER}@${SERVER_HOST}:${SSH_PORT}
|
||||
Статус регистрации: ${SERVER_STATUS}
|
||||
Сети за клиентом: ${ADVERTISE_SUBNETS:-не объявлены}
|
||||
LAN forwarding/NAT: ${FORWARDING_MODE}
|
||||
Лог: ${LOG_FILE}
|
||||
=================================================
|
||||
EOF_SUMMARY
|
||||
@@ -434,6 +485,7 @@ main() {
|
||||
build_allowed_ips
|
||||
build_client_interface_address
|
||||
build_route_hooks_if_needed
|
||||
build_lan_nat_hooks_if_needed
|
||||
write_client_config
|
||||
apply_client_config
|
||||
print_summary
|
||||
|
||||
314
gui/app.py
314
gui/app.py
@@ -2,42 +2,60 @@
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import qrcode
|
||||
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.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_META_FILE = os.environ.get("WG_META_FILE", "/etc/wireguard/wg-meta.env")
|
||||
ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
|
||||
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "")
|
||||
ONLINE_WINDOW_SEC = int(os.environ.get("ONLINE_WINDOW_SEC", "120"))
|
||||
|
||||
|
||||
def db_conn():
|
||||
return connect(DB_DSN, row_factory=dict_row)
|
||||
db_dir = os.path.dirname(DB_PATH)
|
||||
if db_dir:
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def ensure_schema():
|
||||
with db_conn() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS peers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
public_key TEXT UNIQUE NOT NULL,
|
||||
client_address TEXT,
|
||||
advertised_routes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
client_conf TEXT,
|
||||
peer_psk TEXT,
|
||||
peer_allowed_ips TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
""")
|
||||
cols = {row[1] for row in cur.execute("PRAGMA table_info(peers)").fetchall()}
|
||||
if "client_conf" not in cols:
|
||||
cur.execute("ALTER TABLE peers ADD COLUMN client_conf TEXT")
|
||||
if "peer_psk" not in cols:
|
||||
cur.execute("ALTER TABLE peers ADD COLUMN peer_psk TEXT")
|
||||
if "peer_allowed_ips" not in cols:
|
||||
cur.execute("ALTER TABLE peers ADD COLUMN peer_allowed_ips TEXT")
|
||||
if "enabled" not in cols:
|
||||
cur.execute("ALTER TABLE peers ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1")
|
||||
conn.commit()
|
||||
|
||||
|
||||
@@ -69,6 +87,61 @@ def parse_kv(text):
|
||||
return out
|
||||
|
||||
|
||||
def parse_wg_conf_peer_meta():
|
||||
path = f"/etc/wireguard/{WG_INTERFACE}.conf"
|
||||
if not os.path.exists(path):
|
||||
return {}
|
||||
|
||||
by_pub = {}
|
||||
pending_name = None
|
||||
in_peer = False
|
||||
current_pub = ""
|
||||
current_allowed = ""
|
||||
|
||||
def flush():
|
||||
nonlocal current_pub, current_allowed, pending_name
|
||||
if not current_pub:
|
||||
return
|
||||
first = current_allowed.split(",", 1)[0].strip() if current_allowed else ""
|
||||
routes = ""
|
||||
if "," in current_allowed:
|
||||
routes = current_allowed.split(",", 1)[1].strip()
|
||||
by_pub[current_pub] = {
|
||||
"name": pending_name or "(external)",
|
||||
"client_address": first,
|
||||
"routes": routes,
|
||||
"allowed_ips": current_allowed,
|
||||
}
|
||||
pending_name = None
|
||||
current_pub = ""
|
||||
current_allowed = ""
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for raw in f:
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("# managed-by=wg-peerctl"):
|
||||
marker = "client="
|
||||
if marker in line:
|
||||
pending_name = line.split(marker, 1)[1].split()[0].strip()
|
||||
continue
|
||||
if line == "[Peer]":
|
||||
if in_peer:
|
||||
flush()
|
||||
in_peer = True
|
||||
continue
|
||||
if in_peer and line.startswith("PublicKey"):
|
||||
current_pub = line.split("=", 1)[1].strip()
|
||||
continue
|
||||
if in_peer and line.startswith("AllowedIPs"):
|
||||
current_allowed = line.split("=", 1)[1].strip()
|
||||
continue
|
||||
if in_peer:
|
||||
flush()
|
||||
return by_pub
|
||||
|
||||
|
||||
def wg_dump():
|
||||
try:
|
||||
out = run(["wg", "show", WG_INTERFACE, "dump"])
|
||||
@@ -80,15 +153,18 @@ def wg_dump():
|
||||
parts = line.split("\t")
|
||||
if len(parts) < 8:
|
||||
continue
|
||||
latest_ts = 0
|
||||
latest = "never"
|
||||
if parts[5].isdigit() and int(parts[5]) > 0:
|
||||
latest = datetime.utcfromtimestamp(int(parts[5])).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
latest_ts = int(parts[5])
|
||||
latest = datetime.utcfromtimestamp(latest_ts).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
peers.append(
|
||||
{
|
||||
"public_key": parts[0],
|
||||
"endpoint": parts[2] or "-",
|
||||
"allowed_ips": parts[3],
|
||||
"latest_handshake": latest,
|
||||
"latest_handshake_ts": latest_ts,
|
||||
"rx_bytes": int(parts[6] or 0),
|
||||
"tx_bytes": int(parts[7] or 0),
|
||||
}
|
||||
@@ -115,7 +191,7 @@ def gen_keypair_psk():
|
||||
def to_png_b64(text):
|
||||
img = qrcode.make(text)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
img.save(buf)
|
||||
return base64.b64encode(buf.getvalue()).decode("ascii")
|
||||
|
||||
|
||||
@@ -150,17 +226,23 @@ def _schema():
|
||||
def index():
|
||||
meta = load_meta()
|
||||
runtime = {p["public_key"]: p for p in wg_dump()}
|
||||
with db_conn() as conn, conn.cursor() as cur:
|
||||
conf_meta = parse_wg_conf_peer_meta()
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM peers ORDER BY id DESC")
|
||||
db_peers = cur.fetchall()
|
||||
db_peers = [dict(r) for r in cur.fetchall()]
|
||||
|
||||
items = []
|
||||
seen = set()
|
||||
now = int(time.time())
|
||||
for row in db_peers:
|
||||
rt = runtime.get(row["public_key"], {})
|
||||
ts = int(rt.get("latest_handshake_ts", 0) or 0)
|
||||
is_online = ts > 0 and (now - ts) <= ONLINE_WINDOW_SEC
|
||||
seen.add(row["public_key"])
|
||||
items.append(
|
||||
{
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"public_key": row["public_key"],
|
||||
"client_address": row.get("client_address") or "-",
|
||||
@@ -170,25 +252,46 @@ def index():
|
||||
"latest_handshake": rt.get("latest_handshake", "offline"),
|
||||
"rx": bytes_h(rt.get("rx_bytes", 0)),
|
||||
"tx": bytes_h(rt.get("tx_bytes", 0)),
|
||||
"status": "online" if rt else "offline",
|
||||
"status": "online" if is_online else "offline",
|
||||
}
|
||||
)
|
||||
|
||||
for pk, rt in runtime.items():
|
||||
if pk in seen:
|
||||
continue
|
||||
cm = conf_meta.get(pk, {})
|
||||
imported_name = cm.get("name", "(external)")
|
||||
imported_addr = cm.get("client_address", rt.get("allowed_ips", "-").split(",", 1)[0])
|
||||
imported_routes = cm.get("routes", "-") or "-"
|
||||
|
||||
ext_id = None
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT OR IGNORE INTO peers(name, public_key, client_address, advertised_routes, enabled) VALUES (?,?,?,?,1)",
|
||||
(imported_name, pk, imported_addr, imported_routes if imported_routes != "-" else ""),
|
||||
)
|
||||
cur.execute("SELECT id FROM peers WHERE public_key = ?", (pk,))
|
||||
got = cur.fetchone()
|
||||
if got:
|
||||
ext_id = got["id"]
|
||||
conn.commit()
|
||||
|
||||
ts = int(rt.get("latest_handshake_ts", 0) or 0)
|
||||
is_online = ts > 0 and (now - ts) <= ONLINE_WINDOW_SEC
|
||||
items.append(
|
||||
{
|
||||
"name": "(external)",
|
||||
"id": ext_id,
|
||||
"name": imported_name,
|
||||
"public_key": pk,
|
||||
"client_address": rt.get("allowed_ips", "-").split(",", 1)[0],
|
||||
"routes": "-",
|
||||
"client_address": imported_addr,
|
||||
"routes": imported_routes,
|
||||
"allowed_ips": rt.get("allowed_ips", "-"),
|
||||
"endpoint": rt.get("endpoint", "-"),
|
||||
"latest_handshake": rt.get("latest_handshake", "offline"),
|
||||
"rx": bytes_h(rt.get("rx_bytes", 0)),
|
||||
"tx": bytes_h(rt.get("tx_bytes", 0)),
|
||||
"status": "online",
|
||||
"status": "online" if is_online else "offline",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -260,15 +363,16 @@ def new_peer():
|
||||
client_conf = "\n".join(conf_lines)
|
||||
qr_b64 = to_png_b64(client_conf)
|
||||
|
||||
with db_conn() as conn, conn.cursor() as cur:
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO peers(name, public_key, client_address, advertised_routes)
|
||||
VALUES (%s,%s,%s,%s)
|
||||
ON CONFLICT(public_key)
|
||||
DO UPDATE SET name=excluded.name, client_address=excluded.client_address, advertised_routes=excluded.advertised_routes
|
||||
""",
|
||||
(name, client_pub, client_addr, routes),
|
||||
"UPDATE peers SET name=?, client_address=?, advertised_routes=?, client_conf=?, peer_psk=?, peer_allowed_ips=?, enabled=1 WHERE public_key=?",
|
||||
(name, client_addr, routes, client_conf, client_psk, client_addr + (("," + routes) if routes else ""), client_pub),
|
||||
)
|
||||
if cur.rowcount == 0:
|
||||
cur.execute(
|
||||
"INSERT INTO peers(name, public_key, client_address, advertised_routes, client_conf, peer_psk, peer_allowed_ips, enabled) VALUES (?,?,?,?,?,?,?,1)",
|
||||
(name, client_pub, client_addr, routes, client_conf, client_psk, client_addr + (("," + routes) if routes else "")),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -281,6 +385,160 @@ def new_peer():
|
||||
)
|
||||
|
||||
|
||||
@app.route("/peers/<int:peer_id>")
|
||||
def peer_view(peer_id: int):
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM peers WHERE id = ?", (peer_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
flash("Клиент не найден", "error")
|
||||
return redirect(url_for("index"))
|
||||
item = dict(row)
|
||||
|
||||
conf = item.get("client_conf") or ""
|
||||
if not conf:
|
||||
flash("Для этого клиента не найден сохраненный конфиг", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
qr_b64 = to_png_b64(conf)
|
||||
return render_template(
|
||||
"peer_created.html",
|
||||
name=item.get("name", "peer"),
|
||||
client_conf=conf,
|
||||
qr_b64=qr_b64,
|
||||
public_key=item.get("public_key", ""),
|
||||
)
|
||||
|
||||
|
||||
@app.post("/peers/<int:peer_id>/disable")
|
||||
def peer_disable(peer_id: int):
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM peers WHERE id = ?", (peer_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
flash("Клиент не найден", "error")
|
||||
return redirect(url_for("index"))
|
||||
item = dict(row)
|
||||
|
||||
pk = item.get("public_key", "")
|
||||
if not pk:
|
||||
flash("Не найден public key", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
run(["/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
except subprocess.CalledProcessError as e:
|
||||
flash(f"Не удалось отключить peer: {e}", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("UPDATE peers SET enabled=0 WHERE id = ?", (peer_id,))
|
||||
conn.commit()
|
||||
flash("Peer отключен", "ok")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.post("/peers/<int:peer_id>/enable")
|
||||
def peer_enable(peer_id: int):
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM peers WHERE id = ?", (peer_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
flash("Клиент не найден", "error")
|
||||
return redirect(url_for("index"))
|
||||
item = dict(row)
|
||||
|
||||
name = item.get("name", "")
|
||||
pk = item.get("public_key", "")
|
||||
addr = item.get("client_address", "")
|
||||
routes = item.get("advertised_routes", "") or ""
|
||||
psk = item.get("peer_psk", "") or ""
|
||||
if not (name and pk and addr and psk):
|
||||
flash("Недостаточно данных для включения peer (нужны name/public key/address/psk)", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
cmd = [
|
||||
"/usr/local/sbin/wg-peerctl",
|
||||
"add",
|
||||
"--client-name",
|
||||
name,
|
||||
"--client-public-key",
|
||||
pk,
|
||||
"--client-address",
|
||||
addr,
|
||||
"--client-preshared-key",
|
||||
psk,
|
||||
"--persistent-keepalive",
|
||||
"25",
|
||||
]
|
||||
if routes:
|
||||
cmd += ["--client-routes", routes]
|
||||
|
||||
try:
|
||||
run(cmd)
|
||||
except subprocess.CalledProcessError as e:
|
||||
flash(f"Не удалось включить peer: {e}", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("UPDATE peers SET enabled=1 WHERE id = ?", (peer_id,))
|
||||
conn.commit()
|
||||
flash("Peer включен", "ok")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.post("/peers/<int:peer_id>/delete")
|
||||
def peer_delete(peer_id: int):
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM peers WHERE id = ?", (peer_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
flash("Клиент не найден", "error")
|
||||
return redirect(url_for("index"))
|
||||
item = dict(row)
|
||||
|
||||
pk = item.get("public_key", "")
|
||||
if pk:
|
||||
try:
|
||||
run(["/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM peers WHERE id = ?", (peer_id,))
|
||||
conn.commit()
|
||||
flash("Peer удален", "ok")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.post("/peers/delete-by-key")
|
||||
def peer_delete_by_key():
|
||||
pk = (request.form.get("public_key") or "").strip()
|
||||
if not pk:
|
||||
flash("Не найден public key", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
run(["/usr/local/sbin/wg-peerctl", "remove", "--client-public-key", pk])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with db_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM peers WHERE public_key = ?", (pk,))
|
||||
conn.commit()
|
||||
|
||||
flash("Peer удален", "ok")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.route("/scripts")
|
||||
def scripts():
|
||||
commands = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
Flask==3.0.3
|
||||
psycopg[binary]==3.2.1
|
||||
qrcode==7.4.2
|
||||
gunicorn==23.0.0
|
||||
|
||||
@@ -14,6 +14,7 @@ main { padding: 22px; }
|
||||
label { display: grid; gap: 6px; }
|
||||
input, select, button { padding: 10px; border-radius: 10px; border: 1px solid #bad2bf; font-size: 14px; }
|
||||
button { background: var(--brand); color: #fff; border: 0; font-weight: 700; cursor: pointer; }
|
||||
button.danger { background: #b42318; }
|
||||
table { width: 100%; border-collapse: collapse; background: white; border: 1px solid #d7e7da; }
|
||||
th, td { border-bottom: 1px solid #e0ece2; padding: 8px; font-size: 13px; text-align: left; vertical-align: top; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th><th>Статус</th><th>IP</th><th>Роуты</th><th>AllowedIPs</th><th>Endpoint</th><th>Handshake</th><th>RX</th><th>TX</th><th>PubKey</th>
|
||||
<th>Имя</th><th>Статус</th><th>IP</th><th>Роуты</th><th>AllowedIPs</th><th>Endpoint</th><th>Handshake</th><th>RX</th><th>TX</th><th>PubKey</th><th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -21,6 +21,19 @@
|
||||
<td>{{ p.rx }}</td>
|
||||
<td>{{ p.tx }}</td>
|
||||
<td class="mono">{{ p.public_key }}</td>
|
||||
<td>
|
||||
{% if p.id %}
|
||||
<a href="{{ url_for('peer_view', peer_id=p.id) }}">QR/Config</a>
|
||||
<form method="post" action="{{ url_for('peer_delete', peer_id=p.id) }}" style="display:inline" onsubmit="return confirm('Удалить peer?')">
|
||||
<button type="submit" class="danger">Удалить</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('peer_delete_by_key') }}" style="display:inline" onsubmit="return confirm('Удалить peer?')">
|
||||
<input type="hidden" name="public_key" value="{{ p.public_key }}" />
|
||||
<button type="submit" class="danger">Удалить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -6,17 +6,41 @@
|
||||
<input name="name" required placeholder="astra" />
|
||||
</label>
|
||||
<label>Режим
|
||||
<select name="mode">
|
||||
<select name="mode" id="mode">
|
||||
<option value="full">full (весь трафик через VPN)</option>
|
||||
<option value="split">split (только выбранные сети)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>AllowedIPs (для split)
|
||||
<input name="allowed_ips" placeholder="{{ meta.get('WG_NETWORK','10.66.66.0/24') }}" />
|
||||
<input
|
||||
id="allowed_ips"
|
||||
name="allowed_ips"
|
||||
placeholder="{{ meta.get('WG_NETWORK','10.66.66.0/24') }}"
|
||||
data-default="{{ meta.get('WG_NETWORK','10.66.66.0/24') }}"
|
||||
/>
|
||||
</label>
|
||||
<label>Сети за клиентом (роуты)
|
||||
<input name="routes" placeholder="192.168.33.0/24,10.10.0.0/16" />
|
||||
</label>
|
||||
<button type="submit">Создать</button>
|
||||
</form>
|
||||
<script>
|
||||
(() => {
|
||||
const mode = document.getElementById("mode");
|
||||
const allowed = document.getElementById("allowed_ips");
|
||||
const def = allowed.dataset.default || "10.66.66.0/24";
|
||||
|
||||
function syncAllowed() {
|
||||
if (mode.value === "split") {
|
||||
if (!allowed.value.trim()) allowed.value = def;
|
||||
allowed.readOnly = false;
|
||||
} else {
|
||||
allowed.readOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
mode.addEventListener("change", syncAllowed);
|
||||
syncAllowed();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% block content %}
|
||||
<h2>Peer создан: {{ name }}</h2>
|
||||
<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>
|
||||
<h3>QR</h3>
|
||||
|
||||
@@ -24,7 +24,6 @@ GUI_USER="admin"
|
||||
GUI_PASSWORD=""
|
||||
GUI_PASSWORD_GENERATED=0
|
||||
GUI_RESET_DB="no"
|
||||
GUI_DB_PASSWORD=""
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
@@ -116,16 +115,6 @@ validate_inputs() {
|
||||
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() {
|
||||
log_warn "Выполняю полный reset предыдущей инсталляции WireGuard/GUI"
|
||||
|
||||
@@ -149,14 +138,9 @@ reset_existing_install() {
|
||||
rm -f /etc/systemd/system/wg-admin-gui.service
|
||||
|
||||
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
|
||||
detect_compose_cmd
|
||||
(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
|
||||
(cd /opt/wireguard-ui && docker-compose down --remove-orphans >/dev/null 2>&1) || true
|
||||
fi
|
||||
|
||||
docker rm -f wireguard-ui wg-admin-postgres >/dev/null 2>&1 || true
|
||||
@@ -213,8 +197,6 @@ collect_inputs() {
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
GUI_DB_PASSWORD="$(random_alnum 24)"
|
||||
[[ "$GUI_RESET_DB" == "yes" || "$GUI_RESET_DB" == "no" ]] || die "--gui-reset-db должен быть yes или no"
|
||||
fi
|
||||
|
||||
@@ -224,15 +206,7 @@ collect_inputs() {
|
||||
install_packages() {
|
||||
apt_install_if_missing \
|
||||
wireguard wireguard-tools iproute2 iptables curl ca-certificates openssl \
|
||||
docker.io 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
|
||||
python3 python3-venv python3-pip
|
||||
}
|
||||
|
||||
setup_sysctl() {
|
||||
@@ -371,37 +345,17 @@ EOF_SYNC_PATH
|
||||
setup_gui() {
|
||||
[[ "$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
|
||||
|
||||
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
|
||||
/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
|
||||
|
||||
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_META_FILE=/etc/wireguard/wg-meta.env
|
||||
ADMIN_USER=${GUI_USER}
|
||||
@@ -414,7 +368,7 @@ EOF_ENV
|
||||
cat > /etc/systemd/system/wg-admin-gui.service <<EOF_SERVICE
|
||||
[Unit]
|
||||
Description=WG Admin GUI
|
||||
After=network-online.target docker.service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
|
||||
@@ -15,6 +15,7 @@ fi
|
||||
|
||||
LOG_FILE="/var/log/wireguard-peerctl.log"
|
||||
WG_META_FILE="/etc/wireguard/wg-meta.env"
|
||||
GUI_DB_FILE="/opt/wg-admin-gui/data/wgadmin.db"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
@@ -27,12 +28,89 @@ usage() {
|
||||
[--client-preshared-key <psk>] \
|
||||
[--persistent-keepalive 25]
|
||||
|
||||
wg-peerctl.sh remove \
|
||||
--client-public-key <pubkey>
|
||||
|
||||
Описание:
|
||||
Скрипт добавляет peer в конфигурацию WireGuard-сервера идемпотентно.
|
||||
Если peer с таким public key уже существует, повторно не добавляет.
|
||||
USAGE
|
||||
}
|
||||
|
||||
sql_escape() {
|
||||
local s="$1"
|
||||
s="${s//\'/\'\'}"
|
||||
printf "%s" "$s"
|
||||
}
|
||||
|
||||
ensure_gui_db_schema() {
|
||||
command -v sqlite3 >/dev/null 2>&1 || return 0
|
||||
[[ -f "$GUI_DB_FILE" ]] || return 0
|
||||
sqlite3 "$GUI_DB_FILE" <<'SQL' >/dev/null 2>&1 || true
|
||||
CREATE TABLE IF NOT EXISTS peers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
public_key TEXT UNIQUE NOT NULL,
|
||||
client_address TEXT,
|
||||
advertised_routes TEXT,
|
||||
client_conf TEXT,
|
||||
peer_psk TEXT,
|
||||
peer_allowed_ips TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
ALTER TABLE peers ADD COLUMN client_conf TEXT;
|
||||
ALTER TABLE peers ADD COLUMN peer_psk TEXT;
|
||||
ALTER TABLE peers ADD COLUMN peer_allowed_ips TEXT;
|
||||
ALTER TABLE peers ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
|
||||
SQL
|
||||
}
|
||||
|
||||
sync_gui_db_upsert_peer() {
|
||||
local name="$1"
|
||||
local pubkey="$2"
|
||||
local address="$3"
|
||||
local routes="$4"
|
||||
local psk="$5"
|
||||
local peer_allowed_ips="$6"
|
||||
local enabled="${7:-1}"
|
||||
|
||||
command -v sqlite3 >/dev/null 2>&1 || return 0
|
||||
[[ -f "$GUI_DB_FILE" ]] || return 0
|
||||
ensure_gui_db_schema
|
||||
|
||||
local e_name e_pub e_addr e_routes e_psk e_allowed
|
||||
e_name="$(sql_escape "$name")"
|
||||
e_pub="$(sql_escape "$pubkey")"
|
||||
e_addr="$(sql_escape "$address")"
|
||||
e_routes="$(sql_escape "$routes")"
|
||||
e_psk="$(sql_escape "$psk")"
|
||||
e_allowed="$(sql_escape "$peer_allowed_ips")"
|
||||
|
||||
sqlite3 "$GUI_DB_FILE" <<SQL >/dev/null 2>&1 || true
|
||||
INSERT INTO peers(name, public_key, client_address, advertised_routes, peer_psk, peer_allowed_ips, enabled)
|
||||
VALUES ('$e_name', '$e_pub', '$e_addr', '$e_routes', '$e_psk', '$e_allowed', $enabled)
|
||||
ON CONFLICT(public_key)
|
||||
DO UPDATE SET
|
||||
name=excluded.name,
|
||||
client_address=excluded.client_address,
|
||||
advertised_routes=excluded.advertised_routes,
|
||||
peer_psk=excluded.peer_psk,
|
||||
peer_allowed_ips=excluded.peer_allowed_ips,
|
||||
enabled=excluded.enabled;
|
||||
SQL
|
||||
}
|
||||
|
||||
sync_gui_db_set_enabled() {
|
||||
local pubkey="$1"
|
||||
local enabled="$2"
|
||||
command -v sqlite3 >/dev/null 2>&1 || return 0
|
||||
[[ -f "$GUI_DB_FILE" ]] || return 0
|
||||
local e_pub
|
||||
e_pub="$(sql_escape "$pubkey")"
|
||||
sqlite3 "$GUI_DB_FILE" "UPDATE peers SET enabled=${enabled} WHERE public_key='${e_pub}';" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
load_meta() {
|
||||
[[ -f "$WG_META_FILE" ]] || die "Не найден $WG_META_FILE. Сначала выполните install_server.sh"
|
||||
# shellcheck disable=SC1090
|
||||
@@ -129,6 +207,71 @@ extract_peer_address_by_pubkey() {
|
||||
' "$WG_CONF" | awk -F',' '{print $1}' | xargs
|
||||
}
|
||||
|
||||
extract_peer_allowed_ips_by_pubkey() {
|
||||
local pubkey="$1"
|
||||
awk -v pk="$pubkey" '
|
||||
$0 ~ /^\[Peer\]/ {in_peer=1; key=""; allowed=""}
|
||||
in_peer && $0 ~ /^PublicKey[[:space:]]*=/ {
|
||||
sub(/^[^=]*=[[:space:]]*/, "", $0); key=$0
|
||||
}
|
||||
in_peer && $0 ~ /^AllowedIPs[[:space:]]*=/ {
|
||||
sub(/^[^=]*=[[:space:]]*/, "", $0); allowed=$0
|
||||
}
|
||||
in_peer && key==pk && allowed!="" {print allowed; exit}
|
||||
' "$WG_CONF" | xargs
|
||||
}
|
||||
|
||||
routes_without_primary_address() {
|
||||
local allowed_ips="$1"
|
||||
local primary_addr="$2"
|
||||
local out=""
|
||||
local item
|
||||
local norm_primary
|
||||
norm_primary="$(echo "$primary_addr" | xargs)"
|
||||
|
||||
IFS=',' read -ra items <<< "$allowed_ips"
|
||||
for item in "${items[@]}"; do
|
||||
item="$(echo "$item" | xargs)"
|
||||
[[ -z "$item" ]] && continue
|
||||
if [[ -n "$norm_primary" && "$item" == "$norm_primary" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -z "$out" ]]; then
|
||||
out="$item"
|
||||
else
|
||||
out="${out},${item}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
apply_client_routes_now() {
|
||||
local routes="${1:-}"
|
||||
[[ -n "$routes" ]] || return 0
|
||||
|
||||
local cidr
|
||||
IFS=',' read -ra cidrs <<< "$routes"
|
||||
for cidr in "${cidrs[@]}"; do
|
||||
cidr="$(echo "$cidr" | xargs)"
|
||||
[[ -n "$cidr" ]] || continue
|
||||
ip route replace "$cidr" dev "$WG_INTERFACE" proto static >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
|
||||
remove_client_routes_now() {
|
||||
local routes="${1:-}"
|
||||
[[ -n "$routes" ]] || return 0
|
||||
|
||||
local cidr
|
||||
IFS=',' read -ra cidrs <<< "$routes"
|
||||
for cidr in "${cidrs[@]}"; do
|
||||
cidr="$(echo "$cidr" | xargs)"
|
||||
[[ -n "$cidr" ]] || continue
|
||||
ip route del "$cidr" dev "$WG_INTERFACE" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
|
||||
apply_config() {
|
||||
if systemctl is-active --quiet "wg-quick@${WG_INTERFACE}"; then
|
||||
wg syncconf "$WG_INTERFACE" <(wg-quick strip "$WG_CONF")
|
||||
@@ -179,7 +322,14 @@ cmd_add() {
|
||||
|
||||
if peer_exists_by_pubkey "$client_pubkey"; then
|
||||
local existing_addr
|
||||
local existing_allowed
|
||||
existing_addr="$(extract_peer_address_by_pubkey "$client_pubkey")"
|
||||
existing_allowed="${existing_addr:-}"
|
||||
if [[ -n "$client_routes" ]]; then
|
||||
existing_allowed="${existing_allowed},${client_routes}"
|
||||
apply_client_routes_now "$client_routes"
|
||||
fi
|
||||
sync_gui_db_upsert_peer "$client_name" "$client_pubkey" "${existing_addr:-}" "$client_routes" "$client_psk" "${existing_allowed}" 1
|
||||
cat <<EOF_OUT
|
||||
STATUS=exists
|
||||
CLIENT_NAME=$client_name
|
||||
@@ -219,6 +369,8 @@ EOF_OUT
|
||||
} >> "$WG_CONF"
|
||||
|
||||
apply_config
|
||||
apply_client_routes_now "$client_routes"
|
||||
sync_gui_db_upsert_peer "$client_name" "$client_pubkey" "$client_address" "$client_routes" "$client_psk" "$peer_allowed_ips" 1
|
||||
|
||||
cat <<EOF_OUT
|
||||
STATUS=created
|
||||
@@ -232,6 +384,65 @@ WG_NETWORK=${WG_NETWORK}
|
||||
EOF_OUT
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
local client_pubkey=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--client-public-key)
|
||||
client_pubkey="$2"; shift 2 ;;
|
||||
*)
|
||||
die "Неизвестный аргумент: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$client_pubkey" ]] || die "Не указан --client-public-key"
|
||||
|
||||
load_meta
|
||||
[[ -f "$WG_CONF" ]] || die "Не найден конфиг WireGuard: $WG_CONF"
|
||||
local existing_allowed existing_addr existing_routes
|
||||
existing_allowed="$(extract_peer_allowed_ips_by_pubkey "$client_pubkey")"
|
||||
existing_addr="$(extract_peer_address_by_pubkey "$client_pubkey")"
|
||||
existing_routes="$(routes_without_primary_address "$existing_allowed" "$existing_addr")"
|
||||
backup_file "$WG_CONF"
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
awk -v pk="$client_pubkey" '
|
||||
BEGIN {in=0; block=""; keep=1}
|
||||
/^\[Peer\]/ {
|
||||
if (in && keep) printf "%s", block
|
||||
in=1; block=$0 ORS; keep=1; next
|
||||
}
|
||||
{
|
||||
if (in) {
|
||||
block = block $0 ORS
|
||||
if ($0 ~ /^PublicKey[[:space:]]*=/) {
|
||||
line=$0
|
||||
sub(/^[^=]*=[[:space:]]*/, "", line)
|
||||
if (line == pk) keep=0
|
||||
}
|
||||
next
|
||||
}
|
||||
print
|
||||
}
|
||||
END {
|
||||
if (in && keep) printf "%s", block
|
||||
}
|
||||
' "$WG_CONF" > "$tmp"
|
||||
mv "$tmp" "$WG_CONF"
|
||||
safe_chmod_600 "$WG_CONF"
|
||||
|
||||
apply_config
|
||||
remove_client_routes_now "$existing_routes"
|
||||
sync_gui_db_set_enabled "$client_pubkey" 0
|
||||
|
||||
cat <<EOF_OUT
|
||||
STATUS=removed
|
||||
PUBLIC_KEY=${client_pubkey}
|
||||
WG_INTERFACE=${WG_INTERFACE}
|
||||
EOF_OUT
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
if [[ -z "$cmd" ]]; then
|
||||
@@ -246,6 +457,11 @@ main() {
|
||||
check_os_supported
|
||||
cmd_add "$@"
|
||||
;;
|
||||
remove)
|
||||
require_root
|
||||
check_os_supported
|
||||
cmd_remove "$@"
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
|
||||
Reference in New Issue
Block a user