feat: route commands for peer clients (Linux/Windows) + footer signature
- scripts page: new card with Linux/macOS and Windows route commands per peer that has advertised_routes, with OS tab switcher and copy buttons - Made by Galyaviev moved from sidebar to bottom-center page footer on all pages - page-footer style: Dancing Script 20px, clickable mailto:ruslan@ipcom.su Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+31
-2
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import base64
|
import base64
|
||||||
import io
|
import io
|
||||||
|
import ipaddress
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -658,6 +659,15 @@ def peer_rename(peer_id: int):
|
|||||||
return ("", 204)
|
return ("", 204)
|
||||||
|
|
||||||
|
|
||||||
|
def _cidr_parts(cidr: str):
|
||||||
|
"""Return (network_address, netmask) for a CIDR string."""
|
||||||
|
try:
|
||||||
|
net = ipaddress.ip_network(cidr.strip(), strict=False)
|
||||||
|
return str(net.network_address), str(net.netmask)
|
||||||
|
except Exception:
|
||||||
|
return cidr.strip(), ""
|
||||||
|
|
||||||
|
|
||||||
@app.route("/scripts")
|
@app.route("/scripts")
|
||||||
def scripts():
|
def scripts():
|
||||||
commands = {
|
commands = {
|
||||||
@@ -666,14 +676,33 @@ def scripts():
|
|||||||
"apply_wg": "wg syncconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf)",
|
"apply_wg": "wg syncconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf)",
|
||||||
}
|
}
|
||||||
paths = [
|
paths = [
|
||||||
"sudo", "/usr/local/sbin/wg-peerctl",
|
"/usr/local/sbin/wg-peerctl",
|
||||||
"/etc/wireguard/wg0.conf",
|
"/etc/wireguard/wg0.conf",
|
||||||
"/etc/wireguard/wg-meta.env",
|
"/etc/wireguard/wg-meta.env",
|
||||||
"/var/log/wireguard-server-install.log",
|
"/var/log/wireguard-server-install.log",
|
||||||
"/var/log/wireguard-client-install.log",
|
"/var/log/wireguard-client-install.log",
|
||||||
"/var/log/wireguard-peerctl.log",
|
"/var/log/wireguard-peerctl.log",
|
||||||
]
|
]
|
||||||
return render_template("scripts.html", commands=commands, paths=paths)
|
|
||||||
|
route_peers = []
|
||||||
|
with db_conn() as conn:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT name, client_address, advertised_routes FROM peers "
|
||||||
|
"WHERE advertised_routes IS NOT NULL AND advertised_routes != '' ORDER BY name"
|
||||||
|
)
|
||||||
|
for row in cur.fetchall():
|
||||||
|
wg_ip = (row["client_address"] or "").split("/")[0]
|
||||||
|
routes = []
|
||||||
|
for r in (row["advertised_routes"] or "").split(","):
|
||||||
|
r = r.strip()
|
||||||
|
if r:
|
||||||
|
net, mask = _cidr_parts(r)
|
||||||
|
routes.append({"cidr": r, "net": net, "mask": mask})
|
||||||
|
if wg_ip and routes:
|
||||||
|
route_peers.append({"name": row["name"], "wg_ip": wg_ip, "routes": routes})
|
||||||
|
|
||||||
|
return render_template("scripts.html", commands=commands, paths=paths, route_peers=route_peers)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -103,24 +103,25 @@ body {
|
|||||||
|
|
||||||
.logout-btn:hover { background: #fee2e2; color: #dc2626; }
|
.logout-btn:hover { background: #fee2e2; color: #dc2626; }
|
||||||
|
|
||||||
.made-by {
|
.page-footer {
|
||||||
font-family: 'Dancing Script', cursive;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #b0b8cc;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 8px 0 4px;
|
padding: 18px 32px 24px;
|
||||||
|
font-family: 'Dancing Script', cursive;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #c4cad8;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.made-by a {
|
.page-footer a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.made-by a:hover { color: var(--accent); }
|
.page-footer a:hover { color: var(--accent); }
|
||||||
|
|
||||||
/* ─── Layout ────────────────────────────────────────────────── */
|
/* ─── Layout ────────────────────────────────────────────────── */
|
||||||
.layout { margin-left: var(--sidebar-w); flex: 1; display: flex; flex-direction: column; }
|
.layout { margin-left: var(--sidebar-w); flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
|
||||||
main { padding: 28px 32px; flex: 1; max-width: 1400px; width: 100%; }
|
main { padding: 28px 32px; flex: 1; max-width: 1400px; width: 100%; }
|
||||||
|
|
||||||
/* ─── Page header ───────────────────────────────────────────── */
|
/* ─── Page header ───────────────────────────────────────────── */
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||||
Выйти
|
Выйти
|
||||||
</a>
|
</a>
|
||||||
<div class="made-by"><a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
@@ -49,6 +48,9 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,13 +12,58 @@
|
|||||||
<div class="script-label">{{ key }}</div>
|
<div class="script-label">{{ key }}</div>
|
||||||
<div class="script-cmd-wrap">
|
<div class="script-cmd-wrap">
|
||||||
<pre class="script-cmd">{{ cmd }}</pre>
|
<pre class="script-cmd">{{ cmd }}</pre>
|
||||||
<button class="btn btn-sm copy-btn" onclick="copyText(this, '{{ cmd | replace("'", "\\'") }}')">Копировать</button>
|
<button class="btn btn-sm copy-btn" onclick="copyText(this, {{ cmd | tojson }})">Копировать</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if route_peers %}
|
||||||
|
<div class="card">
|
||||||
|
<h3>Маршруты к клиентам за peer</h3>
|
||||||
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:18px;">
|
||||||
|
Команды для других VPN-клиентов, чтобы они могли достучаться до сетей за peer-ом.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="os-tabs" style="display:flex;gap:6px;margin-bottom:18px;">
|
||||||
|
<button class="btn btn-sm os-tab active" data-os="linux" onclick="switchOS('linux', this)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
Linux / macOS
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm os-tab" data-os="windows" onclick="switchOS('windows', this)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
|
||||||
|
Windows
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="script-list">
|
||||||
|
{% for peer in route_peers %}
|
||||||
|
<div class="script-item">
|
||||||
|
<div class="script-label">{{ peer.name }} <span style="font-weight:400;opacity:.7">— WG IP: {{ peer.wg_ip }}</span></div>
|
||||||
|
|
||||||
|
{% for route in peer.routes %}
|
||||||
|
<div class="script-cmd-wrap" style="margin-bottom:6px;">
|
||||||
|
<pre class="script-cmd os-cmd linux">sudo ip route add {{ route.cidr }} via {{ peer.wg_ip }}</pre>
|
||||||
|
<pre class="script-cmd os-cmd windows" style="display:none">route add {{ route.net }} mask {{ route.mask }} {{ peer.wg_ip }}</pre>
|
||||||
|
<button class="btn btn-sm copy-btn"
|
||||||
|
data-linux="sudo ip route add {{ route.cidr }} via {{ peer.wg_ip }}"
|
||||||
|
data-windows="route add {{ route.net }} mask {{ route.mask }} {{ peer.wg_ip }}"
|
||||||
|
onclick="copyOS(this)">Копировать</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:16px;padding:12px 14px;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;font-size:12.5px;color:#92400e;line-height:1.6;">
|
||||||
|
<strong>Постоянные маршруты:</strong><br>
|
||||||
|
Linux: добавьте в <code>/etc/rc.local</code> или конфиг интерфейса.<br>
|
||||||
|
Windows: добавьте флаг <code>-p</code> → <code>route -p add ...</code> — маршрут сохранится после перезагрузки.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Важные пути</h3>
|
<h3>Важные пути</h3>
|
||||||
<ul class="path-list">
|
<ul class="path-list">
|
||||||
@@ -29,6 +74,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
var currentOS = 'linux';
|
||||||
|
|
||||||
|
function switchOS(os, btn) {
|
||||||
|
currentOS = os;
|
||||||
|
document.querySelectorAll('.os-tab').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
document.querySelectorAll('.os-cmd.linux').forEach(el => el.style.display = os === 'linux' ? '' : 'none');
|
||||||
|
document.querySelectorAll('.os-cmd.windows').forEach(el => el.style.display = os === 'windows' ? '' : 'none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyOS(btn) {
|
||||||
|
var text = btn.getAttribute('data-' + currentOS);
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
btn.textContent = 'Скопировано!';
|
||||||
|
setTimeout(() => btn.textContent = 'Копировать', 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function copyText(btn, text) {
|
function copyText(btn, text) {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
btn.textContent = 'Скопировано!';
|
btn.textContent = 'Скопировано!';
|
||||||
|
|||||||
Reference in New Issue
Block a user