Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
260 lines
13 KiB
HTML
260 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ peer.name }} - WireGuard Admin{% endblock %}
|
|
{% block breadcrumbs %}<span>/</span> <a href="/peers" class="text-blue-600 hover:underline">Peers</a> <span>/</span> <span>{{ peer.name }}</span>{% endblock %}
|
|
{% block content %}
|
|
{% if error %}
|
|
<p class="mb-4 rounded-md bg-red-50 px-3 py-2 text-sm text-red-700">{{ error }}</p>
|
|
{% endif %}
|
|
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
|
<h1 class="flex items-center gap-2 text-2xl font-bold">
|
|
{{ peer.name }}
|
|
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500">{{ iface.name }}</span>
|
|
{% if not peer.has_private_key %}
|
|
<span class="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700">imported</span>
|
|
{% endif %}
|
|
</h1>
|
|
<div class="flex flex-wrap gap-2">
|
|
<form method="post" action="/peers/{{ peer.id }}/toggle">
|
|
<button type="submit"
|
|
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-100">
|
|
{{ "Disable" if peer.enabled else "Enable" }}
|
|
</button>
|
|
</form>
|
|
<form method="post" action="/peers/{{ peer.id }}/rotate"
|
|
onsubmit="return confirm('Rotate keys? The old client config stops working immediately.')">
|
|
<button type="submit"
|
|
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-100">
|
|
Rotate keys
|
|
</button>
|
|
</form>
|
|
<form method="post" action="/peers/{{ peer.id }}/reset-usage"
|
|
onsubmit="return confirm('Reset usage counters to zero?')">
|
|
<button type="submit"
|
|
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-100">
|
|
Reset usage
|
|
</button>
|
|
</form>
|
|
<form method="post" action="/peers/{{ peer.id }}/delete"
|
|
onsubmit="return confirm('Delete peer {{ peer.name }}?')">
|
|
<button type="submit"
|
|
class="rounded-md border border-red-200 bg-white px-3 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50">
|
|
Delete
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid gap-4 lg:grid-cols-3">
|
|
<div class="rounded-xl bg-white p-5 shadow-sm">
|
|
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Details</h3>
|
|
<dl class="space-y-2 text-sm">
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Address</dt>
|
|
<dd class="font-mono text-xs">{{ peer.address }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-slate-500">Public key</dt>
|
|
<dd class="mt-1 break-all font-mono text-xs text-slate-600">{{ peer.public_key }}</dd>
|
|
</div>
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Enabled</dt>
|
|
<dd>
|
|
{% if peer.enabled %}
|
|
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">yes</span>
|
|
{% else %}
|
|
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">no</span>
|
|
{% endif %}
|
|
</dd>
|
|
</div>
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Created</dt>
|
|
<dd>{{ peer.created_at.strftime("%Y-%m-%d %H:%M") }}</dd>
|
|
</div>
|
|
{% if peer.expires_at %}
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Expires</dt>
|
|
<dd>{{ peer.expires_at.strftime("%Y-%m-%d %H:%M") }}</dd>
|
|
</div>
|
|
{% endif %}
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Usage</dt>
|
|
<dd>
|
|
{{ peer.cum_total | fmt_bytes }}
|
|
{% if peer.quota_bytes %} / {{ peer.quota_bytes | fmt_bytes }}
|
|
{% if peer.over_quota %}
|
|
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">over quota</span>
|
|
{% endif %}
|
|
{% endif %}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="rounded-xl bg-white p-5 shadow-sm">
|
|
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Live status</h3>
|
|
{% if peer_status %}
|
|
<dl class="space-y-2 text-sm">
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Status</dt>
|
|
<dd>
|
|
{% if peer_status.online %}
|
|
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">online</span>
|
|
{% else %}
|
|
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500">offline</span>
|
|
{% endif %}
|
|
</dd>
|
|
</div>
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Endpoint</dt>
|
|
<dd class="font-mono text-xs">{{ peer_status.endpoint or "-" }}</dd>
|
|
</div>
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Handshake</dt>
|
|
<dd>{{ peer_status.latest_handshake.strftime("%Y-%m-%d %H:%M:%S") if peer_status.latest_handshake else "never" }}</dd>
|
|
</div>
|
|
<div class="flex items-baseline justify-between gap-2">
|
|
<dt class="text-slate-500">Session traffic</dt>
|
|
<dd>RX {{ peer_status.rx_bytes | fmt_bytes }} / TX {{ peer_status.tx_bytes | fmt_bytes }}</dd>
|
|
</div>
|
|
</dl>
|
|
{% else %}
|
|
<p class="text-sm text-slate-400">Not present on interface (disabled or interface down).</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="rounded-xl bg-white p-5 shadow-sm">
|
|
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Client config</h3>
|
|
{% if peer.has_private_key %}
|
|
<img src="/peers/{{ peer.id }}/qr" alt="QR code" class="mx-auto w-48 rounded-lg border border-slate-200">
|
|
<div class="mt-4 flex justify-center gap-2">
|
|
<a href="/peers/{{ peer.id }}/config"
|
|
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">
|
|
Download .conf
|
|
</a>
|
|
<button type="button" onclick="copyConfig(this)"
|
|
class="rounded-md border border-slate-300 px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100">
|
|
Copy
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-sm text-slate-500">
|
|
This peer was imported from an existing config, so the server only knows its public key.
|
|
The client keeps using its current config and everything works, but the panel cannot
|
|
generate a .conf or QR code.
|
|
</p>
|
|
<p class="mt-3 text-sm text-slate-500">
|
|
To manage the client config from here, rotate its keys and install the new config on the device.
|
|
</p>
|
|
<form method="post" action="/peers/{{ peer.id }}/rotate" class="mt-4 text-center"
|
|
onsubmit="return confirm('Rotate keys? The device stays offline until the new config is installed.')">
|
|
<button type="submit"
|
|
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">
|
|
Rotate keys and generate config
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 rounded-xl bg-white p-5 shadow-sm">
|
|
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Edit</h3>
|
|
<form method="post" action="/peers/{{ peer.id }}/update" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
<label class="block text-sm font-medium text-slate-600">Note
|
|
<input name="note" value="{{ peer.note }}"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
</label>
|
|
<label class="block text-sm font-medium text-slate-600">DNS
|
|
<input name="dns" value="{{ peer.dns }}" placeholder="server default"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
</label>
|
|
<label class="block text-sm font-medium text-slate-600">Quota (GiB)
|
|
<input name="quota_gib" type="number" step="0.1" min="0" placeholder="unlimited"
|
|
value="{{ '%.1f' % (peer.quota_bytes / 1073741824) if peer.quota_bytes else '' }}"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
</label>
|
|
<label class="block text-sm font-medium text-slate-600">Keepalive (s)
|
|
<input name="persistent_keepalive" type="number" min="0" max="3600"
|
|
value="{{ peer.persistent_keepalive if peer.persistent_keepalive is not none else '' }}"
|
|
placeholder="interface default ({{ iface.persistent_keepalive }})"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
<span class="mt-1 block text-xs text-slate-400">0 disables keepalive (fixed-IP site-to-site peers). Empty = interface default.</span>
|
|
</label>
|
|
<label class="block text-sm font-medium text-slate-600">Site subnets behind this peer
|
|
<input name="extra_allowed_ips" value="{{ peer.extra_allowed_ips }}" placeholder="e.g. 192.168.5.0/24"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
<span class="mt-1 block text-xs text-slate-400">Server-side AllowedIPs: routes these networks to this peer (site-to-site). Peer must forward traffic.</span>
|
|
</label>
|
|
<label class="block text-sm font-medium text-slate-600">Client routes (AllowedIPs)
|
|
<input name="client_allowed_ips" value="{{ peer.client_allowed_ips }}" placeholder="server default"
|
|
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
<span class="mt-1 block text-xs text-slate-400">What this client routes through the tunnel. Empty = server default. Full tunnel: 0.0.0.0/0, ::/0</span>
|
|
</label>
|
|
<div class="col-span-full">
|
|
<button type="submit"
|
|
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{% if client_config %}
|
|
<div class="mt-4 rounded-xl bg-white p-5 shadow-sm">
|
|
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">{{ peer.name }}.conf</h3>
|
|
<pre id="client-config" class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs leading-relaxed text-slate-200">{{ client_config }}</pre>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if client_config %}
|
|
<div class="mt-4 rounded-xl bg-white p-5 shadow-sm">
|
|
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Client setup guide</h3>
|
|
<div class="space-y-5 text-sm">
|
|
<div>
|
|
<h4 class="mb-2 font-semibold text-slate-700">Linux (wg-quick)</h4>
|
|
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs leading-relaxed text-slate-200"># Install wireguard tools
|
|
sudo apt install wireguard # Debian/Ubuntu
|
|
sudo dnf install wireguard-tools # Fedora/RHEL
|
|
|
|
# Save the config (downloaded or copied from above)
|
|
sudo mv {{ peer.name }}.conf /etc/wireguard/{{ peer.name }}.conf
|
|
sudo chmod 600 /etc/wireguard/{{ peer.name }}.conf
|
|
|
|
# Bring the tunnel up / down
|
|
sudo wg-quick up {{ peer.name }}
|
|
sudo wg-quick down {{ peer.name }}
|
|
|
|
# Start automatically at boot
|
|
sudo systemctl enable --now wg-quick@{{ peer.name }}
|
|
|
|
# Verify: check handshake and ping the server
|
|
sudo wg show
|
|
ping {{ server_tunnel_ip }}</pre>
|
|
</div>
|
|
<div>
|
|
<h4 class="mb-2 font-semibold text-slate-700">Linux (NetworkManager)</h4>
|
|
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs leading-relaxed text-slate-200">nmcli connection import type wireguard file {{ peer.name }}.conf
|
|
nmcli connection up {{ peer.name }}</pre>
|
|
</div>
|
|
<div>
|
|
<h4 class="mb-2 font-semibold text-slate-700">macOS / Windows</h4>
|
|
<p class="text-slate-500">Install the official WireGuard app, then Import Tunnel from File and select the downloaded <span class="font-mono text-xs">{{ peer.name }}.conf</span>, or Add Empty Tunnel and paste the config above.</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="mb-2 font-semibold text-slate-700">iOS / Android</h4>
|
|
<p class="text-slate-500">Install the WireGuard app, tap Add Tunnel, then Create from QR code and scan the QR code above.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<script>
|
|
function copyConfig(btn) {
|
|
navigator.clipboard.writeText(document.getElementById("client-config").textContent)
|
|
.then(() => {
|
|
btn.textContent = "Copied";
|
|
setTimeout(() => { btn.textContent = "Copy"; }, 1500);
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|