mirror of
https://github.com/andrey271192/domen_hydra.git
synced 2026-09-20 14:42:00 +00:00
feat: WireGuard VPN генератор
- Новая вкладка «🔒 WireGuard VPN» в панели - Авто-установка WireGuard сервера на VPS (apt + systemd wg-quick@wg0) - Генерация ключевых пар (сервер + клиент на каждый роутер) - Выдача IP из подсети 10.8.0.0/24 - Деплой на роутер по SSH: opkg install wireguard-tools + wg-quick + init.d - Попытка нативной интеграции через Keenetic CLI (ndmc) — роутер появится в «Приоритетах подключений»; fallback на wg-quick если ndmc недоступен - Просмотр и копирование конфигов (wg0.conf для VPS и для каждого роутера) - Добавление/удаление пиров с авто-обновлением wg0.conf на VPS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
242
server/main.py
242
server/main.py
@@ -2,6 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -526,3 +527,244 @@ async def tunnel_remove(name: str, x_admin_password: str = Header("")):
|
|||||||
R[key] = rcfg
|
R[key] = rcfg
|
||||||
save_json(config.ROUTERS_FILE, R)
|
save_json(config.ROUTERS_FILE, R)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ── WireGuard VPN ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_WG_DATA = config.DATA_DIR / "wireguard.json"
|
||||||
|
_WG_SUBNET = "10.8.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_wg() -> dict:
|
||||||
|
return load_json(_WG_DATA, {"server": {}, "peers": {}})
|
||||||
|
|
||||||
|
|
||||||
|
def _save_wg(d: dict):
|
||||||
|
save_json(_WG_DATA, d)
|
||||||
|
|
||||||
|
|
||||||
|
def _wg_genkey() -> tuple[str, str]:
|
||||||
|
r = subprocess.run(["wg", "genkey"], capture_output=True, text=True, timeout=5)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError("wg не найден. На VPS: apt install wireguard-tools")
|
||||||
|
priv = r.stdout.strip()
|
||||||
|
pub = subprocess.run(["wg", "pubkey"], input=priv, capture_output=True, text=True, timeout=5).stdout.strip()
|
||||||
|
return priv, pub
|
||||||
|
|
||||||
|
|
||||||
|
def _wg_next_ip(peers: dict) -> str:
|
||||||
|
used = {int(v["ip"].split(".")[-1]) for v in peers.values() if v.get("ip")}
|
||||||
|
for i in range(2, 255):
|
||||||
|
if i not in used:
|
||||||
|
return f"{_WG_SUBNET}.{i}"
|
||||||
|
raise RuntimeError("Нет свободных IP в WG-подсети")
|
||||||
|
|
||||||
|
|
||||||
|
def _wg_server_conf(data: dict) -> str:
|
||||||
|
srv = data["server"]
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["ip", "route", "get", "8.8.8.8"], capture_output=True, text=True, timeout=5)
|
||||||
|
m = re.search(r"dev (\S+)", r.stdout)
|
||||||
|
iface = m.group(1) if m else "eth0"
|
||||||
|
except Exception:
|
||||||
|
iface = "eth0"
|
||||||
|
lines = [
|
||||||
|
"[Interface]",
|
||||||
|
f"PrivateKey = {srv['private_key']}",
|
||||||
|
f"Address = {_WG_SUBNET}.1/24",
|
||||||
|
f"ListenPort = {srv.get('port', 51820)}",
|
||||||
|
f"PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o {iface} -j MASQUERADE",
|
||||||
|
f"PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o {iface} -j MASQUERADE",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for rname, peer in data.get("peers", {}).items():
|
||||||
|
lines += [f"# {rname}", "[Peer]", f"PublicKey = {peer['public_key']}",
|
||||||
|
f"AllowedIPs = {peer['ip']}/32", ""]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _wg_client_conf(data: dict, rkey: str, vps_host: str) -> str:
|
||||||
|
srv = data["server"]
|
||||||
|
peer = data["peers"][rkey]
|
||||||
|
return (
|
||||||
|
"[Interface]\n"
|
||||||
|
f"PrivateKey = {peer['private_key']}\n"
|
||||||
|
f"Address = {peer['ip']}/32\n"
|
||||||
|
"DNS = 8.8.8.8\n\n"
|
||||||
|
"[Peer]\n"
|
||||||
|
f"PublicKey = {srv['public_key']}\n"
|
||||||
|
f"Endpoint = {vps_host}:{srv.get('port', 51820)}\n"
|
||||||
|
"AllowedIPs = 0.0.0.0/0\n"
|
||||||
|
"PersistentKeepalive = 25\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _wg_reload(conf: str):
|
||||||
|
p = Path("/etc/wireguard/wg0.conf")
|
||||||
|
p.write_text(conf)
|
||||||
|
p.chmod(0o600)
|
||||||
|
subprocess.run(
|
||||||
|
["bash", "-c", "wg syncconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf) 2>/dev/null || true"],
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/wireguard")
|
||||||
|
async def wg_get(x_admin_password: str = Header("")):
|
||||||
|
_chk(x_admin_password)
|
||||||
|
data = _load_wg()
|
||||||
|
srv = data.get("server", {})
|
||||||
|
running = False
|
||||||
|
if srv.get("private_key"):
|
||||||
|
try:
|
||||||
|
running = subprocess.run(["wg", "show", "wg0"], capture_output=True, timeout=5).returncode == 0
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
"initialized": bool(srv.get("private_key")),
|
||||||
|
"running": running,
|
||||||
|
"public_key": srv.get("public_key", ""),
|
||||||
|
"port": srv.get("port", 51820),
|
||||||
|
"peers": {k: {"ip": v.get("ip"), "public_key": v.get("public_key")}
|
||||||
|
for k, v in data.get("peers", {}).items()},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/wireguard/init")
|
||||||
|
async def wg_init_server(x_admin_password: str = Header("")):
|
||||||
|
_chk(x_admin_password)
|
||||||
|
data = _load_wg()
|
||||||
|
if not data["server"].get("private_key"):
|
||||||
|
priv, pub = await asyncio.to_thread(_wg_genkey)
|
||||||
|
data["server"] = {"private_key": priv, "public_key": pub, "port": 51820}
|
||||||
|
_save_wg(data)
|
||||||
|
conf = _wg_server_conf(data)
|
||||||
|
|
||||||
|
def _setup():
|
||||||
|
subprocess.run(["apt-get", "install", "-y", "--no-install-recommends", "wireguard"],
|
||||||
|
capture_output=True, timeout=120)
|
||||||
|
subprocess.run(["sysctl", "-w", "net.ipv4.ip_forward=1"], capture_output=True, timeout=5)
|
||||||
|
Path("/etc/sysctl.d/99-wg.conf").write_text("net.ipv4.ip_forward=1\n")
|
||||||
|
Path("/etc/wireguard").mkdir(parents=True, exist_ok=True)
|
||||||
|
_wg_reload(conf)
|
||||||
|
subprocess.run(["systemctl", "enable", "wg-quick@wg0"], capture_output=True, timeout=10)
|
||||||
|
subprocess.run(["systemctl", "restart", "wg-quick@wg0"], capture_output=True, timeout=30)
|
||||||
|
|
||||||
|
await asyncio.to_thread(_setup)
|
||||||
|
running = subprocess.run(["wg", "show", "wg0"], capture_output=True, timeout=5).returncode == 0
|
||||||
|
return {"ok": True, "running": running, "public_key": data["server"]["public_key"]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/wireguard/server-config", response_class=PlainTextResponse)
|
||||||
|
async def wg_server_config(x_admin_password: str = Header("")):
|
||||||
|
_chk(x_admin_password)
|
||||||
|
data = _load_wg()
|
||||||
|
if not data["server"].get("private_key"):
|
||||||
|
raise HTTPException(400, "WireGuard не инициализирован")
|
||||||
|
return _wg_server_conf(data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/routers/{name}/wireguard")
|
||||||
|
async def wg_add_peer(name: str, x_admin_password: str = Header("")):
|
||||||
|
_chk(x_admin_password)
|
||||||
|
data = _load_wg()
|
||||||
|
if not data["server"].get("private_key"):
|
||||||
|
raise HTTPException(400, "Сначала инициализируй WireGuard сервер")
|
||||||
|
key = _router_key(name)
|
||||||
|
peers = data.setdefault("peers", {})
|
||||||
|
if key not in peers:
|
||||||
|
priv, pub = await asyncio.to_thread(_wg_genkey)
|
||||||
|
peers[key] = {"private_key": priv, "public_key": pub, "ip": _wg_next_ip(peers)}
|
||||||
|
_save_wg(data)
|
||||||
|
await asyncio.to_thread(_wg_reload, _wg_server_conf(data))
|
||||||
|
return {"ok": True, "ip": peers[key]["ip"], "public_key": peers[key]["public_key"]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/routers/{name}/wireguard-config", response_class=PlainTextResponse)
|
||||||
|
async def wg_router_config(name: str, x_admin_password: str = Header("")):
|
||||||
|
_chk(x_admin_password)
|
||||||
|
data = _load_wg()
|
||||||
|
key = _router_key(name)
|
||||||
|
if key not in data.get("peers", {}):
|
||||||
|
raise HTTPException(404, "Пир не найден")
|
||||||
|
return _wg_client_conf(data, key, config.VPS_SSH_HOST or "VPS_IP")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/routers/{name}/wireguard/deploy")
|
||||||
|
async def wg_deploy(name: str, x_admin_password: str = Header("")):
|
||||||
|
_chk(x_admin_password)
|
||||||
|
data = _load_wg()
|
||||||
|
key = _router_key(name)
|
||||||
|
if key not in data.get("peers", {}):
|
||||||
|
raise HTTPException(400, "Сначала добавь роутер в WireGuard")
|
||||||
|
vps_host = config.VPS_SSH_HOST or ""
|
||||||
|
if not vps_host:
|
||||||
|
raise HTTPException(400, "VPS_SSH_HOST не задан в .env")
|
||||||
|
rcfg = _get_router_cfg(name)
|
||||||
|
peer = data["peers"][key]
|
||||||
|
srv = data["server"]
|
||||||
|
wg_conf = _wg_client_conf(data, key, vps_host)
|
||||||
|
priv_key = peer["private_key"]
|
||||||
|
pub_key = srv["public_key"]
|
||||||
|
port = srv.get("port", 51820)
|
||||||
|
client_ip = peer["ip"]
|
||||||
|
|
||||||
|
script = f"""\
|
||||||
|
set -e
|
||||||
|
echo '[1/3] Установка wireguard-tools...'
|
||||||
|
opkg update 2>/dev/null || true
|
||||||
|
opkg install wireguard-tools kmod-wireguard 2>/dev/null || true
|
||||||
|
command -v wg >/dev/null 2>&1 || {{ echo 'ОШИБКА: wg не установлен'; exit 1; }}
|
||||||
|
|
||||||
|
echo '[2/3] Запись конфига...'
|
||||||
|
mkdir -p /opt/etc/wireguard
|
||||||
|
cat > /opt/etc/wireguard/wg0.conf << 'WGEOF'
|
||||||
|
{wg_conf}WGEOF
|
||||||
|
chmod 600 /opt/etc/wireguard/wg0.conf
|
||||||
|
cat > /opt/etc/init.d/S50wg0 << 'INITEOF'
|
||||||
|
#!/bin/sh
|
||||||
|
case "$1" in
|
||||||
|
start) wg-quick up /opt/etc/wireguard/wg0.conf 2>/dev/null ;;
|
||||||
|
stop) wg-quick down /opt/etc/wireguard/wg0.conf 2>/dev/null ;;
|
||||||
|
restart) wg-quick down /opt/etc/wireguard/wg0.conf 2>/dev/null; wg-quick up /opt/etc/wireguard/wg0.conf ;;
|
||||||
|
esac
|
||||||
|
INITEOF
|
||||||
|
chmod +x /opt/etc/init.d/S50wg0
|
||||||
|
|
||||||
|
echo '[3/3] Запуск VPN...'
|
||||||
|
wg-quick down /opt/etc/wireguard/wg0.conf 2>/dev/null || true
|
||||||
|
wg-quick up /opt/etc/wireguard/wg0.conf
|
||||||
|
|
||||||
|
if command -v ndmc >/dev/null 2>&1; then
|
||||||
|
printf '%s\\n' \\
|
||||||
|
'interface Wireguard0' \\
|
||||||
|
'description HydraVPN' \\
|
||||||
|
'wireguard private-key {priv_key}' \\
|
||||||
|
'wireguard peer {pub_key}' \\
|
||||||
|
'wireguard allowed-ips 0.0.0.0 0.0.0.0' \\
|
||||||
|
'wireguard endpoint {vps_host} {port}' \\
|
||||||
|
'wireguard persistent-keepalive 25' \\
|
||||||
|
'exit' \\
|
||||||
|
'system configuration save' | ndmc 2>/dev/null && \\
|
||||||
|
echo 'Keenetic CLI: нативная интеграция OK (проверь в Подключениях)' || \\
|
||||||
|
echo 'Keenetic CLI: не удалось, wg-quick запущен'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo '=== OK ==='
|
||||||
|
echo 'VPN IP роутера: {client_ip}'
|
||||||
|
ip addr show wg0 2>/dev/null | grep inet || wg show wg0 2>/dev/null || true
|
||||||
|
"""
|
||||||
|
|
||||||
|
rc, out, err = await _ssh_on_router(rcfg, script, timeout=120)
|
||||||
|
return {"ok": rc == 0, "output": (out + err)[:1000]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/routers/{name}/wireguard")
|
||||||
|
async def wg_remove_peer(name: str, x_admin_password: str = Header("")):
|
||||||
|
_chk(x_admin_password)
|
||||||
|
data = _load_wg()
|
||||||
|
key = _router_key(name)
|
||||||
|
data.get("peers", {}).pop(key, None)
|
||||||
|
_save_wg(data)
|
||||||
|
await asyncio.to_thread(_wg_reload, _wg_server_conf(data))
|
||||||
|
return {"ok": True}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ input[type=text]:focus,input[type=password]:focus{border-color:var(--accent)}
|
|||||||
<button class="tab active" onclick="showTab('view')">📋 Конфигурация</button>
|
<button class="tab active" onclick="showTab('view')">📋 Конфигурация</button>
|
||||||
<button class="tab" onclick="showTab('import')">⬆ Импорт файлов</button>
|
<button class="tab" onclick="showTab('import')">⬆ Импорт файлов</button>
|
||||||
<button class="tab" onclick="showTab('routers')">🔧 Роутеры</button>
|
<button class="tab" onclick="showTab('routers')">🔧 Роутеры</button>
|
||||||
|
<button class="tab" onclick="showTab('vpn')">🔒 WireGuard VPN</button>
|
||||||
<button class="tab" onclick="showTab('settings')">⚙ Настройки</button>
|
<button class="tab" onclick="showTab('settings')">⚙ Настройки</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -241,6 +242,39 @@ input[type=text]:focus,input[type=password]:focus{border-color:var(--accent)}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- VPN TAB -->
|
||||||
|
<div id="tab-vpn" style="display:none">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-head">
|
||||||
|
<h2>🖥 WireGuard сервер (VPS)</h2>
|
||||||
|
<button class="btn btn-ghost" onclick="loadWg()" style="font-size:11px">↺ Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div id="wg-server-card"><span style="color:var(--muted)">Загрузка...</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="wg-routers-section" style="display:none">
|
||||||
|
<div class="section-head">
|
||||||
|
<h2>📡 Роутеры в VPN</h2>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:var(--muted);margin-bottom:12px">Добавь роутер — сгенерируются ключи и IP. Потом нажми «Установить на роутер» — скрипт сам настроит WireGuard через SSH.</p>
|
||||||
|
<div id="wg-routers-list"></div>
|
||||||
|
<div id="wg-ops-log" style="display:none;background:var(--card2);border-radius:10px;padding:12px;font-size:11px;font-family:monospace;white-space:pre-wrap;max-height:220px;overflow-y:auto;margin-top:12px;color:var(--text)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WG CONFIG MODAL -->
|
||||||
|
<div id="wg-cfg-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:1000;align-items:center;justify-content:center">
|
||||||
|
<div style="background:var(--card);border-radius:18px;padding:24px;max-width:620px;width:94%;position:relative">
|
||||||
|
<button onclick="document.getElementById('wg-cfg-modal').style.display='none'" style="position:absolute;top:14px;right:16px;background:none;border:none;color:var(--muted);font-size:20px;cursor:pointer">✕</button>
|
||||||
|
<h2 style="font-size:15px;font-weight:700;margin-bottom:12px" id="wg-cfg-title">WireGuard конфиг</h2>
|
||||||
|
<textarea id="wg-cfg-text" readonly style="width:100%;min-height:260px;background:var(--card2);border:1px solid var(--border);border-radius:10px;padding:12px;font-size:11px;font-family:monospace;color:var(--text);resize:vertical;outline:none"></textarea>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:12px">
|
||||||
|
<button class="btn btn-b" onclick="copyWgCfg()">📋 Копировать</button>
|
||||||
|
<button class="btn btn-ghost" onclick="document.getElementById('wg-cfg-modal').style.display='none'">✓ Закрыть</button>
|
||||||
|
</div>
|
||||||
|
<div id="wg-cfg-hint" style="margin-top:10px;font-size:11px;color:var(--muted)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SETTINGS TAB -->
|
<!-- SETTINGS TAB -->
|
||||||
<div id="tab-settings" style="display:none">
|
<div id="tab-settings" style="display:none">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@@ -278,12 +312,13 @@ function logout(){ sessionStorage.removeItem('hm_pass'); _showLogin(); }
|
|||||||
function _afterLogin(){ loadConfig(); }
|
function _afterLogin(){ loadConfig(); }
|
||||||
|
|
||||||
// ── TABS ──────────────────────────────────────────────────────────────────
|
// ── TABS ──────────────────────────────────────────────────────────────────
|
||||||
const TABS = ['view','import','routers','settings'];
|
const TABS = ['view','import','routers','vpn','settings'];
|
||||||
function showTab(t){
|
function showTab(t){
|
||||||
TABS.forEach(id=>{ document.getElementById('tab-'+id).style.display=id===t?'':'none'; });
|
TABS.forEach(id=>{ document.getElementById('tab-'+id).style.display=id===t?'':'none'; });
|
||||||
document.querySelectorAll('.tab').forEach((el,i)=>el.classList.toggle('active',i===TABS.indexOf(t)));
|
document.querySelectorAll('.tab').forEach((el,i)=>el.classList.toggle('active',i===TABS.indexOf(t)));
|
||||||
if(t==='import') loadExport();
|
if(t==='import') loadExport();
|
||||||
if(t==='routers') loadRouters();
|
if(t==='routers') loadRouters();
|
||||||
|
if(t==='vpn') loadWg();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CONFIG ────────────────────────────────────────────────────────────────
|
// ── CONFIG ────────────────────────────────────────────────────────────────
|
||||||
@@ -498,6 +533,149 @@ async function checkTunnelStatus(){
|
|||||||
}catch(e){el.textContent='❌ '+e; el.style.color='var(--red)';}
|
}catch(e){el.textContent='❌ '+e; el.style.color='var(--red)';}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── WIREGUARD VPN ─────────────────────────────────────────────────────────
|
||||||
|
let WG = null;
|
||||||
|
|
||||||
|
async function loadWg(){
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/wireguard', {headers: authHdr()});
|
||||||
|
WG = await r.json();
|
||||||
|
renderWgServer();
|
||||||
|
await renderWgRouters();
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('wg-server-card').innerHTML = '<span style="color:var(--red)">Ошибка загрузки: '+e+'</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWgServer(){
|
||||||
|
const el = document.getElementById('wg-server-card');
|
||||||
|
if(!WG.initialized){
|
||||||
|
el.innerHTML = `
|
||||||
|
<p style="font-size:13px;color:var(--muted);margin-bottom:14px">WireGuard не установлен на VPS. Нажми кнопку — скрипт установит <code>wireguard</code>, сгенерирует ключи и запустит сервер.</p>
|
||||||
|
<button class="btn btn-b" onclick="wgInitServer()" id="wg-init-btn">⚡ Установить WireGuard на VPS</button>
|
||||||
|
<div id="wg-init-msg" class="msg"></div>`;
|
||||||
|
} else {
|
||||||
|
const statusColor = WG.running ? 'var(--green)' : 'var(--red)';
|
||||||
|
const statusText = WG.running ? '✅ Работает (wg0)' : '❌ Остановлен';
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start">
|
||||||
|
<div style="min-width:120px">
|
||||||
|
<div style="font-size:11px;color:var(--muted);margin-bottom:2px">Статус</div>
|
||||||
|
<div style="font-weight:700;color:${statusColor}">${statusText}</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted);margin-top:6px">Порт</div>
|
||||||
|
<div style="font-weight:600">${WG.port}</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;min-width:200px">
|
||||||
|
<div style="font-size:11px;color:var(--muted);margin-bottom:2px">Публичный ключ VPS (для клиентов)</div>
|
||||||
|
<div style="font-family:monospace;font-size:11px;word-break:break-all;background:var(--card2);padding:8px 10px;border-radius:8px">${WG.public_key}</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px">
|
||||||
|
${!WG.running ? `<button class="btn btn-b" onclick="wgInitServer()">⚡ Запустить</button>` : ''}
|
||||||
|
<button class="btn btn-ghost" onclick="wgShowServerConfig()" style="font-size:11px">📄 wg0.conf</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
document.getElementById('wg-routers-section').style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderWgRouters(){
|
||||||
|
if(!WG || !WG.initialized) return;
|
||||||
|
let routers = {};
|
||||||
|
try { routers = await (await fetch('/api/routers', {headers: authHdr()})).json(); } catch(e){}
|
||||||
|
const el = document.getElementById('wg-routers-list');
|
||||||
|
const entries = Object.entries(routers);
|
||||||
|
if(!entries.length){ el.innerHTML = '<p style="color:var(--muted);font-size:12px">Нет роутеров. Добавь их во вкладке «Роутеры».</p>'; return; }
|
||||||
|
el.innerHTML = entries.map(([name, r]) => {
|
||||||
|
const peer = WG.peers[name];
|
||||||
|
const hasPeer = !!peer;
|
||||||
|
return `<div style="background:var(--card2);border-radius:12px;padding:12px 14px;margin-bottom:8px;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||||
|
<div style="font-weight:700;font-size:13px;min-width:110px">${name}</div>
|
||||||
|
<div style="font-size:12px;flex:1">
|
||||||
|
${hasPeer
|
||||||
|
? `<span style="color:var(--green)">● VPN IP: <b>${peer.ip}</b></span>`
|
||||||
|
: `<span style="color:var(--muted)">— не добавлен</span>`}
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||||
|
${!hasPeer
|
||||||
|
? `<button class="btn btn-b" style="font-size:11px" onclick="wgAddPeer('${name}')">+ В VPN</button>`
|
||||||
|
: `<button class="btn btn-g" style="font-size:11px" onclick="wgDeploy('${name}')">📡 Установить на роутер</button>
|
||||||
|
<button class="btn btn-ghost" style="font-size:11px" onclick="wgShowRouterConfig('${name}')">📄 Конфиг</button>
|
||||||
|
<button class="btn btn-r" style="font-size:11px" onclick="wgRemovePeer('${name}')">✕ Убрать</button>`}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wgInitServer(){
|
||||||
|
const btn = document.getElementById('wg-init-btn');
|
||||||
|
if(btn){ btn.disabled=true; btn.textContent='Устанавливаю (~1 мин)...'; }
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/wireguard/init', {method:'POST', headers: authHdr()});
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.ok){ await loadWg(); }
|
||||||
|
else { alert('Ошибка: '+(d.detail||JSON.stringify(d))); if(btn){btn.disabled=false;btn.textContent='⚡ Установить WireGuard на VPS';} }
|
||||||
|
} catch(e){ alert('Ошибка: '+e); if(btn){btn.disabled=false;btn.textContent='⚡ Установить WireGuard на VPS';} }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wgAddPeer(name){
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/routers/${encodeURIComponent(name)}/wireguard`, {method:'POST', headers: authHdr()});
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.ok){ await loadWg(); }
|
||||||
|
else alert('Ошибка: '+(d.detail||JSON.stringify(d)));
|
||||||
|
} catch(e){ alert('Ошибка: '+e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wgRemovePeer(name){
|
||||||
|
if(!confirm(`Удалить ${name} из WireGuard VPN?`)) return;
|
||||||
|
try {
|
||||||
|
await fetch(`/api/routers/${encodeURIComponent(name)}/wireguard`, {method:'DELETE', headers: authHdr()});
|
||||||
|
await loadWg();
|
||||||
|
} catch(e){ alert('Ошибка: '+e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wgDeploy(name){
|
||||||
|
const log = document.getElementById('wg-ops-log');
|
||||||
|
log.style.display = 'block';
|
||||||
|
log.textContent = `Устанавливаю WireGuard на роутер «${name}»...\n`;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/routers/${encodeURIComponent(name)}/wireguard/deploy`, {method:'POST', headers: authHdr()});
|
||||||
|
const d = await r.json();
|
||||||
|
log.textContent += d.output || '';
|
||||||
|
if(!d.ok) log.textContent += '\n❌ Завершилось с ошибкой';
|
||||||
|
else log.textContent += '\n✅ Готово';
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
} catch(e){ log.textContent += '\nОшибка: '+e; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wgShowServerConfig(){
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/wireguard/server-config', {headers: authHdr()});
|
||||||
|
const text = await r.text();
|
||||||
|
document.getElementById('wg-cfg-title').textContent = 'wg0.conf — конфиг VPS сервера';
|
||||||
|
document.getElementById('wg-cfg-text').value = text;
|
||||||
|
document.getElementById('wg-cfg-hint').textContent = 'Этот файл лежит на VPS в /etc/wireguard/wg0.conf';
|
||||||
|
document.getElementById('wg-cfg-modal').style.display = 'flex';
|
||||||
|
} catch(e){ alert('Ошибка: '+e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wgShowRouterConfig(name){
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/routers/${encodeURIComponent(name)}/wireguard-config`, {headers: authHdr()});
|
||||||
|
const text = await r.text();
|
||||||
|
document.getElementById('wg-cfg-title').textContent = `wg0.conf — конфиг роутера «${name}»`;
|
||||||
|
document.getElementById('wg-cfg-text').value = text;
|
||||||
|
document.getElementById('wg-cfg-hint').textContent = 'Можно вручную добавить в Keenetic: Интернет → WireGuard → Добавить подключение → Вставить из буфера';
|
||||||
|
document.getElementById('wg-cfg-modal').style.display = 'flex';
|
||||||
|
} catch(e){ alert('Ошибка: '+e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyWgCfg(){
|
||||||
|
const t = document.getElementById('wg-cfg-text').value;
|
||||||
|
if(navigator.clipboard){ navigator.clipboard.writeText(t); }
|
||||||
|
else { document.getElementById('wg-cfg-text').select(); document.execCommand('copy'); }
|
||||||
|
}
|
||||||
|
|
||||||
// ── SETTINGS ──────────────────────────────────────────────────────────────
|
// ── SETTINGS ──────────────────────────────────────────────────────────────
|
||||||
async function changePwd(){ const p1=document.getElementById('new-pwd').value; const p2=document.getElementById('new-pwd2').value; if(p1!==p2){sm('pwd-msg','err','❌ Пароли не совпадают');return;} if(p1.length<4){sm('pwd-msg','err','❌ Минимум 4 символа');return;} try{ const r=await fetch('/api/set_password',{method:'POST',headers:authHdr(),body:JSON.stringify({password:p1})}); if(r.ok){ sessionStorage.setItem('hm_pass',p1); sm('pwd-msg','ok','✅ Пароль сохранён'); document.getElementById('new-pwd').value=''; document.getElementById('new-pwd2').value=''; } }catch(e){sm('pwd-msg','err','❌ '+e)} }
|
async function changePwd(){ const p1=document.getElementById('new-pwd').value; const p2=document.getElementById('new-pwd2').value; if(p1!==p2){sm('pwd-msg','err','❌ Пароли не совпадают');return;} if(p1.length<4){sm('pwd-msg','err','❌ Минимум 4 символа');return;} try{ const r=await fetch('/api/set_password',{method:'POST',headers:authHdr(),body:JSON.stringify({password:p1})}); if(r.ok){ sessionStorage.setItem('hm_pass',p1); sm('pwd-msg','ok','✅ Пароль сохранён'); document.getElementById('new-pwd').value=''; document.getElementById('new-pwd2').value=''; } }catch(e){sm('pwd-msg','err','❌ '+e)} }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user