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
|
||||
import base64
|
||||
import io
|
||||
import ipaddress
|
||||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
@@ -658,6 +659,15 @@ def peer_rename(peer_id: int):
|
||||
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")
|
||||
def scripts():
|
||||
commands = {
|
||||
@@ -666,14 +676,33 @@ def scripts():
|
||||
"apply_wg": "wg syncconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf)",
|
||||
}
|
||||
paths = [
|
||||
"sudo", "/usr/local/sbin/wg-peerctl",
|
||||
"/usr/local/sbin/wg-peerctl",
|
||||
"/etc/wireguard/wg0.conf",
|
||||
"/etc/wireguard/wg-meta.env",
|
||||
"/var/log/wireguard-server-install.log",
|
||||
"/var/log/wireguard-client-install.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__":
|
||||
|
||||
@@ -103,24 +103,25 @@ body {
|
||||
|
||||
.logout-btn:hover { background: #fee2e2; color: #dc2626; }
|
||||
|
||||
.made-by {
|
||||
font-family: 'Dancing Script', cursive;
|
||||
font-size: 18px;
|
||||
color: #b0b8cc;
|
||||
.page-footer {
|
||||
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;
|
||||
}
|
||||
|
||||
.made-by a {
|
||||
.page-footer a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.made-by a:hover { color: var(--accent); }
|
||||
.page-footer a:hover { color: var(--accent); }
|
||||
|
||||
/* ─── 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%; }
|
||||
|
||||
/* ─── 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>
|
||||
Выйти
|
||||
</a>
|
||||
<div class="made-by"><a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="layout">
|
||||
@@ -49,6 +48,9 @@
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer class="page-footer">
|
||||
<a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,13 +12,58 @@
|
||||
<div class="script-label">{{ key }}</div>
|
||||
<div class="script-cmd-wrap">
|
||||
<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>
|
||||
{% endfor %}
|
||||
</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">
|
||||
<h3>Важные пути</h3>
|
||||
<ul class="path-list">
|
||||
@@ -29,6 +74,24 @@
|
||||
</div>
|
||||
|
||||
<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) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
btn.textContent = 'Скопировано!';
|
||||
|
||||
Reference in New Issue
Block a user