From 60e1edc0d5d2ac4316bc32f422215e04e517dedb Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 21 Jun 2026 22:48:46 +0200 Subject: [PATCH] feat(wg): configurable DNS + Server/Peers tabs + faster offline (v0.9.45) Three things the operator asked for on the WireGuard page: - DNS is no longer hardcoded to 1.1.1.1. New host action set-dns (_set_dns + cmd_set_dns + write-bridge branch + WgBridge validation, IPv4 list only), stored as WG_DNS in wg.env (appended for pre-existing tunnels), surfaced via the collector + WgStatus, baked into new peer configs (DNS = ${WG_DNS:-1.1.1.1}). Editable in the Server tab; e.g. point clients at your own gateway. - The page is split into two independent tabs: "Peers" (list / add / remove / traffic) and "Server configuration" (endpoint / DNS / port / subnet / gate / SSH lock + server identity). #[Url] $tab makes it deep-linkable (?tab=server). - Offline detection: ONLINE_WITHIN 180s -> 150s (PersistentKeepalive=25 keeps active peers re-handshaking well within it, so no flapping; a disconnect now shows offline ~2.5 min sooner). Endpoint hint clarified: the port is optional (auto-appended), no need to repeat the listen port. Tests: set-dns (write+audit, reject), tab switch/clamp, dns in status; the two configured-render tests now switch to the server tab for server content. 353 pass, shellcheck clean, Pint clean, both tabs load 200 with no console errors, lang parity 89/89. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 19 +- app/Livewire/Wireguard/Index.php | 28 +++ app/Services/WgBridge.php | 10 +- app/Services/WgStatus.php | 10 +- config/clusev.php | 2 +- docker/wg/clusev-wg.sh | 27 +- lang/de/wireguard.php | 8 +- lang/en/wireguard.php | 8 +- .../views/livewire/wireguard/index.blade.php | 238 ++++++++++-------- tests/Feature/WireguardPageTest.php | 48 +++- 10 files changed, 282 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7834d97..893940b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,24 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._ -## [0.9.44] - 2026-06-21 +## [0.9.45] - 2026-06-21 + +### Hinzugefügt +- **DNS individuell einstellbar.** Der DNS-Server in den Client-Configs war fest auf `1.1.1.1`. Jetzt + in der Server-Konfiguration frei wählbar (z. B. dein eigenes Gateway `10.0.0.1`, mehrere + kommagetrennt) — gilt für künftige Peer-Configs, host-seitig validiert (nur IPv4). Auch per CLI: + `clusev wg set-dns `. Bestehende Tunnel bekommen den Wert beim ersten Setzen ergänzt. +- **WireGuard-Seite in Tabs aufgeteilt.** Zwei unabhängige Reiter: **Peers** (Liste, Anlegen, + Entfernen, Traffic) und **Server-Konfiguration** (Endpoint, DNS, Port, Subnetz, Gate, SSH-Sperre + + Server-Identität). Der aktive Tab ist deep-linkbar (`?tab=server`). So lassen sich Server-Einstellungen + getrennt von der Peer-Verwaltung ändern. + +### Geändert +- **Offline-Erkennung schneller.** Das Handshake-Fenster für „online" wurde von 180 s auf 150 s + gesenkt — ein abgeschalteter Peer wird ~2,5 statt ~3 min nach dem Trennen offline (WireGuard ist + verbindungslos, schneller geht es ohne Flackern aktiver Peers nicht). +- **Endpoint-Hinweis klargestellt:** der Port ist optional (fehlt er, wird der Listen-Port ergänzt) — + man muss ihn nicht doppelt zum Listen-Port eintragen. ### Geändert - **Peer-QR-Code kompakter.** Nach der Vergrößerung in 0.9.42 war der QR zu groß; er ist jetzt auf diff --git a/app/Livewire/Wireguard/Index.php b/app/Livewire/Wireguard/Index.php index 3e0f41d..b25ea2e 100644 --- a/app/Livewire/Wireguard/Index.php +++ b/app/Livewire/Wireguard/Index.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\RateLimiter; use Livewire\Attributes\Layout; use Livewire\Attributes\On; +use Livewire\Attributes\Url; use Livewire\Component; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -29,6 +30,10 @@ class Index extends Component public int $window = self::WINDOWS[0]; + /** Active tab when configured: 'peers' (list + traffic) or 'server' (settings). Deep-linkable. */ + #[Url] + public string $tab = 'peers'; + public string $newPeer = ''; public string $newEndpoint = ''; @@ -37,6 +42,8 @@ class Index extends Component public string $newSubnet = ''; + public string $newDns = ''; + // first-time setup form public string $setupSubnet = '10.99.0.0/24'; @@ -69,6 +76,11 @@ class Index extends Component $this->window = $this->clampWindow($seconds); } + public function setTab(string $tab): void + { + $this->tab = in_array($tab, ['peers', 'server'], true) ? $tab : 'peers'; + } + public function addPeer(WgBridge $bridge): void { $this->validate(['newPeer' => ['required', 'regex:'.self::PEER_NAME_RE]], [ @@ -171,6 +183,22 @@ class Index extends Component $this->newEndpoint = ''; } + /** Set the DNS server(s) baked into future peer configs. Non-destructive (no confirm needed). */ + public function setDns(WgBridge $bridge): void + { + $this->validate(['newDns' => ['required', 'regex:/^\d{1,3}(\.\d{1,3}){3}([,\s]+\d{1,3}(\.\d{1,3}){3})*$/']], [ + 'newDns.regex' => __('wireguard.dns_invalid'), + 'newDns.required' => __('wireguard.dns_invalid'), + ]); + if (! $this->throttle()) { + return; + } + $this->pendingId = $bridge->request('set-dns', ['dns' => $this->newDns]); + $this->pendingAction = 'set-dns'; + $this->audit('wg.set-dns', $this->newDns); + $this->newDns = ''; + } + public function confirmGate(bool $on): void { $this->openConfirm('wgGateToggle', ['on' => $on ? '1' : '0'], diff --git a/app/Services/WgBridge.php b/app/Services/WgBridge.php index 8299089..24f49c5 100644 --- a/app/Services/WgBridge.php +++ b/app/Services/WgBridge.php @@ -12,7 +12,7 @@ use Illuminate\Support\Str; class WgBridge { /** Actions the UI may request; the host whitelists the same set. */ - public const ACTIONS = ['add-peer', 'remove-peer', 'gate-up', 'gate-down', 'gate-ssh-on', 'gate-ssh-off', 'set-endpoint', 'set-port', 'set-subnet', 'setup']; + public const ACTIONS = ['add-peer', 'remove-peer', 'gate-up', 'gate-down', 'gate-ssh-on', 'gate-ssh-off', 'set-endpoint', 'set-port', 'set-subnet', 'set-dns', 'setup']; private function dir(): string { @@ -112,6 +112,14 @@ class WgBridge } return ['subnet' => $subnet]; + case 'set-dns': + $dns = (string) ($args['dns'] ?? ''); + // one or more IPv4 addresses, comma/space separated + if (preg_match('/^\d{1,3}(\.\d{1,3}){3}([,\s]+\d{1,3}(\.\d{1,3}){3})*$/', $dns) !== 1) { + throw new \InvalidArgumentException('invalid dns'); + } + + return ['dns' => $dns]; case 'setup': $subnet = (string) ($args['subnet'] ?? ''); $port = (string) ($args['port'] ?? ''); diff --git a/app/Services/WgStatus.php b/app/Services/WgStatus.php index 4e8945f..380eb40 100644 --- a/app/Services/WgStatus.php +++ b/app/Services/WgStatus.php @@ -12,8 +12,13 @@ class WgStatus /** Status older than this (seconds) means the host collector is offline. */ private const STALE_AFTER = 20; - /** A peer with a handshake within this many seconds is considered online. */ - private const ONLINE_WITHIN = 180; + /** + * A peer with a handshake within this many seconds is considered online. WireGuard is + * connectionless, so "offline" can only be inferred from the handshake ageing out. Client configs + * use PersistentKeepalive = 25, so an active peer re-handshakes well within this window; 150s is + * the floor that detects a disconnect ~2.5 min after it happens without flapping live peers. + */ + private const ONLINE_WITHIN = 150; private function path(): string { @@ -71,6 +76,7 @@ class WgStatus 'server_ip' => (string) ($data['server']['server_ip'] ?? ''), 'port' => (int) ($data['server']['port'] ?? 0), 'endpoint' => (string) ($data['server']['endpoint'] ?? ''), + 'dns' => (string) ($data['server']['dns'] ?? ''), // Not rendered by the page, but part of the /wg-status.json response contract (API consumers). 'pubkey' => (string) ($data['server']['pubkey'] ?? ''), ], diff --git a/config/clusev.php b/config/clusev.php index df0f8a5..9a85392 100644 --- a/config/clusev.php +++ b/config/clusev.php @@ -2,7 +2,7 @@ return [ // First tagged release is v0.1.0 (semantic, not -dev). - 'version' => '0.9.44', + 'version' => '0.9.45', // Deployed commit + branch. install.sh bakes these into .env from the host's .git (the prod // image ships no .git); the Versions page prefers them and falls back to a live .git read in diff --git a/docker/wg/clusev-wg.sh b/docker/wg/clusev-wg.sh index 5b3f03a..3b62d57 100755 --- a/docker/wg/clusev-wg.sh +++ b/docker/wg/clusev-wg.sh @@ -163,7 +163,7 @@ EOF [Interface] PrivateKey = ${cpriv} Address = ${ip}/32 -DNS = 1.1.1.1 +DNS = ${WG_DNS:-1.1.1.1} [Peer] PublicKey = ${WG_SERVER_PUBKEY} @@ -337,6 +337,7 @@ WG_SUBNET=${subnet} WG_SERVER_IP=${server_ip} WG_PORT=${port} WG_ENDPOINT=${endpoint} +WG_DNS=1.1.1.1 WG_SERVER_PUBKEY=${srv_pub} EOF chmod 600 "$WG_ENV" @@ -432,10 +433,11 @@ cmd_serve_request() { [ -n "$id" ] || return 0 action="$(printf '%s' "$json" | grep -oE '"action":"[a-z-]{1,24}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" name="$(printf '%s' "$json" | grep -oE '"name":"[A-Za-z0-9._-]{1,64}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" - local endpoint port subnet + local endpoint port subnet dns endpoint="$(printf '%s' "$json" | grep -oE '"endpoint":"[A-Za-z0-9.:_-]{1,128}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" port="$(printf '%s' "$json" | grep -oE '"port":"[0-9]{1,5}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" subnet="$(printf '%s' "$json" | grep -oE '"subnet":"[0-9./]{1,18}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" + dns="$(printf '%s' "$json" | grep -oE '"dns":"[0-9., ]{1,128}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" local ok=false msg='ok' config='' case "$action" in @@ -458,6 +460,7 @@ cmd_serve_request() { set-endpoint) if [ -n "$endpoint" ] && ( _set_endpoint "$endpoint" ) >/dev/null 2>&1; then ok=true; else msg='set-endpoint-failed'; fi ;; set-port) if [ -n "$port" ] && ( _set_port "$port" ) >/dev/null 2>&1; then ok=true; else msg='set-port-failed'; fi ;; set-subnet) if [ -n "$subnet" ] && ( _set_subnet "$subnet" ) >/dev/null 2>&1; then ok=true; else msg='set-subnet-failed'; fi ;; + set-dns) if [ -n "$dns" ] && ( _set_dns "$dns" ) >/dev/null 2>&1; then ok=true; else msg='set-dns-failed'; fi ;; setup) local sip ep pn sip="$(subnet_first_ip "$subnet" 2>/dev/null || true)" @@ -504,6 +507,19 @@ _set_endpoint() { sed -i "s#^WG_ENDPOINT=.*#WG_ENDPOINT=${ep}#" "$WG_ENV" } +_set_dns() { + local dns="$1"; require_setup + # One or more IPv4 addresses, comma/space separated (e.g. "10.0.0.1" or "1.1.1.1, 1.0.0.1"). + # Only affects NEW peer configs (the DNS line is baked into each client config at creation). + case "$dns" in *[!0-9.,\ ]*|'') echo "bad dns" >&2; return 1 ;; esac + printf '%s' "$dns" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}([,[:space:]]+[0-9]{1,3}(\.[0-9]{1,3}){3})*$' || { echo "bad dns" >&2; return 1; } + if grep -q '^WG_DNS=' "$WG_ENV" 2>/dev/null; then + sed -i "s#^WG_DNS=.*#WG_DNS=${dns}#" "$WG_ENV" + else + printf 'WG_DNS=%s\n' "$dns" >> "$WG_ENV" # older wg.env (pre-DNS) has no line yet + fi +} + _set_port() { local port="$1"; require_setup # Re-validated here: cmd_set_port passes $1 unchecked; also adds the 1-65535 range the bridge regex omits. @@ -532,6 +548,7 @@ _set_subnet() { cmd_set_endpoint() { need_root set-endpoint; local v="${1:-}"; [ -n "$v" ] || die "Endpoint fehlt."; _set_endpoint "$v" || die "Endpoint setzen fehlgeschlagen."; bold "Endpoint gesetzt: ${v}"; } cmd_set_port() { need_root set-port; local v="${1:-}"; [ -n "$v" ] || die "Port fehlt."; _set_port "$v" || die "Port setzen fehlgeschlagen."; bold "Port gesetzt: ${v} (Clients muessen den Port aktualisieren)."; } cmd_set_subnet() { need_root set-subnet; local v="${1:-}"; [ -n "$v" ] || die "Subnetz fehlt."; _set_subnet "$v" || die "Subnetz setzen fehlgeschlagen."; bold "Subnetz gesetzt: ${v} (bestehende Peers muessen neu angelegt werden)."; } +cmd_set_dns() { need_root set-dns; local v="${1:-}"; [ -n "$v" ] || die "DNS fehlt."; _set_dns "$v" || die "DNS ungueltig — nur IPv4, kommagetrennt."; bold "DNS gesetzt: ${v} (gilt fuer kuenftige Peer-Configs)."; } cmd_collect() { mkdir -p "$RUN_DIR" 2>/dev/null || true @@ -581,9 +598,9 @@ cmd_collect() { ')" [ -n "$peers_json" ] || peers_json="[]" - printf '{"at":%s,"configured":true,"gate":{"marker":%s,"chain":%s,"ssh":%s},"wg_quick":"%s","server":{"subnet":"%s","server_ip":"%s","port":%s,"endpoint":"%s","pubkey":"%s"},"peers":%s}\n' \ + printf '{"at":%s,"configured":true,"gate":{"marker":%s,"chain":%s,"ssh":%s},"wg_quick":"%s","server":{"subnet":"%s","server_ip":"%s","port":%s,"endpoint":"%s","dns":"%s","pubkey":"%s"},"peers":%s}\n' \ "$now" "$gate_marker" "$gate_chain" "$ssh_marker" "$wgq" \ - "${WG_SUBNET:-}" "${WG_SERVER_IP:-}" "${WG_PORT:-0}" "${WG_ENDPOINT:-}" "${WG_SERVER_PUBKEY:-}" \ + "${WG_SUBNET:-}" "${WG_SERVER_IP:-}" "${WG_PORT:-0}" "${WG_ENDPOINT:-}" "${WG_DNS:-}" "${WG_SERVER_PUBKEY:-}" \ "$peers_json" > "$tmp" mv -f "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp"; return 0; } chmod 644 "$dest" 2>/dev/null || true @@ -602,6 +619,7 @@ clusev wg — WireGuard-Zugang (Host) clusev wg set-endpoint oeffentlichen Endpoint aendern clusev wg set-port Listen-Port aendern (Clients neu) clusev wg set-subnet Subnetz aendern (Peers neu anlegen) + clusev wg set-dns DNS fuer kuenftige Peer-Configs setzen clusev wg ssh-lock SSH (22) sperren — nur noch ueber den Tunnel (Vorsicht: Aussperr-Gefahr!) clusev wg ssh-unlock SSH-Sperre wieder aufheben @@ -621,6 +639,7 @@ case "$cmd" in set-endpoint) cmd_set_endpoint "$@" ;; set-port) cmd_set_port "$@" ;; set-subnet) cmd_set_subnet "$@" ;; + set-dns) cmd_set_dns "$@" ;; ssh-lock) cmd_ssh_lock "$@" ;; ssh-unlock) cmd_ssh_unlock "$@" ;; gate-apply) need_root gate-apply; gate_apply; ssh_gate_apply ;; # called by clusev-wg-gate.service (both gates re-applied on boot) diff --git a/lang/de/wireguard.php b/lang/de/wireguard.php index b3990f7..05008ef 100644 --- a/lang/de/wireguard.php +++ b/lang/de/wireguard.php @@ -15,7 +15,10 @@ return [ 'server_ip' => 'Server-IP', 'port' => 'Port', 'endpoint' => 'Endpoint', + 'dns' => 'DNS', 'service' => 'Dienst', + 'tab_peers' => 'Peers', + 'tab_server' => 'Server-Konfiguration', 'peers_title' => 'Peers', 'peers_count' => ':count Peer|:count Peers', 'no_peers' => 'Noch keine Peers — per SSH "clusev wg add-peer ".', @@ -66,7 +69,10 @@ return [ 'ssh_lock_off_title' => 'SSH-Sperre aufheben?', 'ssh_lock_off_body' => 'Port 22 wird danach wieder normal erreichbar (vorbehaltlich einer Cloud-/Host-Firewall).', 'endpoint_label' => 'Öffentlicher Endpoint', - 'endpoint_hint' => 'IP oder DNS:Port — betrifft nur künftige Peer-Configs.', + 'endpoint_hint' => 'IP oder Host (Port optional — sonst Listen-Port). Betrifft nur künftige Peer-Configs.', + 'dns_label' => 'DNS-Server', + 'dns_hint' => 'DNS für neue Peer-Configs (z. B. dein eigenes Gateway 10.0.0.1). Mehrere durch Komma. Betrifft nur künftige Clients.', + 'dns_invalid' => 'Ungültige DNS-Adresse(n) — nur IPv4, kommagetrennt.', 'endpoint_invalid' => 'Ungültiger Endpoint.', 'port_label' => 'Listen-Port (UDP)', 'port_hint' => 'Ändern startet den Tunnel neu — alle Clients müssen den Port aktualisieren.', diff --git a/lang/en/wireguard.php b/lang/en/wireguard.php index 9a6d8e1..fdef99f 100644 --- a/lang/en/wireguard.php +++ b/lang/en/wireguard.php @@ -15,7 +15,10 @@ return [ 'server_ip' => 'Server IP', 'port' => 'Port', 'endpoint' => 'Endpoint', + 'dns' => 'DNS', 'service' => 'Service', + 'tab_peers' => 'Peers', + 'tab_server' => 'Server configuration', 'peers_title' => 'Peers', 'peers_count' => ':count peer|:count peers', 'no_peers' => 'No peers yet — over SSH run "clusev wg add-peer ".', @@ -66,7 +69,10 @@ return [ 'ssh_lock_off_title' => 'Unlock SSH?', 'ssh_lock_off_body' => 'Port 22 becomes reachable normally again (subject to any cloud/host firewall).', 'endpoint_label' => 'Public endpoint', - 'endpoint_hint' => 'IP or DNS:port — affects only future peer configs.', + 'endpoint_hint' => 'IP or host (port optional — otherwise the listen port). Affects only future peer configs.', + 'dns_label' => 'DNS server', + 'dns_hint' => 'DNS for new peer configs (e.g. your own gateway 10.0.0.1). Multiple comma-separated. Only affects future clients.', + 'dns_invalid' => 'Invalid DNS address(es) — IPv4 only, comma-separated.', 'endpoint_invalid' => 'Invalid endpoint.', 'port_label' => 'Listen port (UDP)', 'port_hint' => 'Changing this restarts the tunnel — all clients must update the port.', diff --git a/resources/views/livewire/wireguard/index.blade.php b/resources/views/livewire/wireguard/index.blade.php index 7ecd318..66ac9c6 100644 --- a/resources/views/livewire/wireguard/index.blade.php +++ b/resources/views/livewire/wireguard/index.blade.php @@ -75,109 +75,21 @@ @endif - @php - $win = collect($windows)->mapWithKeys(fn ($s) => [$s => match ($s) { - 3600 => __('wireguard.window_1h'), 86400 => __('wireguard.window_24h'), default => __('wireguard.window_7d'), - }])->all(); - $pts = $traffic['points']; - $n = max(1, count($pts) - 1); - $peak = max(1, $traffic['peak_down'], $traffic['peak_up']); - $line = function (string $key) use ($pts, $n, $peak) { - $out = []; - foreach ($pts as $i => $p) { - $x = round($i / $n * 600, 1); - $y = round(140 - ($p[$key] / $peak) * 132, 1); - $out[] = $x.','.$y; - } - return implode(' ', $out); - }; - $hasTraffic = $traffic['total_down'] > 0 || $traffic['total_up'] > 0; - @endphp - - -
-
- {{ __('wireguard.total_down') }}: {{ $fmtBytes($traffic['total_down']) }} - {{ __('wireguard.total_up') }}: {{ $fmtBytes($traffic['total_up']) }} -
-
- @foreach ($windows as $w) - - @endforeach -
-
-
- @if ($hasTraffic) - - - - - @else -

{{ __('wireguard.no_traffic') }}

- @endif -
-
- -
- -
- - - {{ __('wireguard.add_peer') }} - - @error('newPeer') {{ $message }} @enderror -
-
- @forelse ($status['peers'] as $peer) -
- - - {{ $peer['name'] !== '' ? $peer['name'] : \Illuminate\Support\Str::limit($peer['pubkey'], 10, '…') }} - - {{ $peer['online'] ? __('wireguard.online') : __('wireguard.offline') }} - {{ __('wireguard.last_handshake') }}: {{ $ago($peer['latest_handshake']) }} - - {{ $peer['endpoint'] !== '' ? $peer['endpoint'] : '—' }} - {{ __('wireguard.down_up', ['rx' => $fmtBytes($peer['rx']), 'tx' => $fmtBytes($peer['tx'])]) }} - -
- - {{ __('wireguard.remove') }} - -
-
- @empty -

{{ __('wireguard.no_peers') }}

- @endforelse -
-
- -
- - @if ($status['gate']['marker'] && $status['gate']['chain']) - {{ __('wireguard.gate_on') }} - @else - {{ __('wireguard.gate_off') }} - @endif -

{{ __('wireguard.gate_escape') }}

-
- - -
-
{{ __('wireguard.subnet') }}
{{ $status['server']['subnet'] ?: '—' }}
-
{{ __('wireguard.server_ip') }}
{{ $status['server']['server_ip'] ?: '—' }}
-
{{ __('wireguard.port') }}
{{ $status['server']['port'] ?: '—' }}
-
{{ __('wireguard.endpoint') }}
{{ $status['server']['endpoint'] ?: '—' }}
-
{{ __('wireguard.service') }}
{{ $status['wg_quick'] }}
-
-
+ {{-- Tabs: Peers (list + traffic) and Server (independent server configuration) --}} +
+ @foreach ([['peers', __('wireguard.tab_peers')], ['server', __('wireguard.tab_server')]] as [$tabKey, $tabLabel]) + + @endforeach +
+ @if ($tab === 'server') + {{-- ── Server configuration tab ──────────────────────────────────────────────── --}} +
{{-- Gate toggle --}} @@ -231,6 +143,21 @@
+ {{-- DNS --}} +
+
+ +
+ + {{ __('wireguard.apply') }} +
+

{{ __('wireguard.dns_hint') }}

+ @error('newDns') {{ $message }} @enderror +
+
+ {{-- Port --}}
@@ -262,8 +189,113 @@
+ +
+ +
+
{{ __('wireguard.subnet') }}
{{ $status['server']['subnet'] ?: '—' }}
+
{{ __('wireguard.server_ip') }}
{{ $status['server']['server_ip'] ?: '—' }}
+
{{ __('wireguard.port') }}
{{ $status['server']['port'] ?: '—' }}
+
{{ __('wireguard.endpoint') }}
{{ $status['server']['endpoint'] ?: '—' }}
+
{{ __('wireguard.dns') }}
{{ $status['server']['dns'] ?: '1.1.1.1' }}
+
{{ __('wireguard.service') }}
{{ $status['wg_quick'] }}
+
+
+ + + @if ($status['gate']['marker'] && $status['gate']['chain']) + {{ __('wireguard.gate_on') }} + @else + {{ __('wireguard.gate_off') }} + @endif +

{{ __('wireguard.gate_escape') }}

+
+
-
+ @else + {{-- ── Peers tab (traffic + peer management) ──────────────────────────────────── --}} + @php + $win = collect($windows)->mapWithKeys(fn ($s) => [$s => match ($s) { + 3600 => __('wireguard.window_1h'), 86400 => __('wireguard.window_24h'), default => __('wireguard.window_7d'), + }])->all(); + $pts = $traffic['points']; + $n = max(1, count($pts) - 1); + $peak = max(1, $traffic['peak_down'], $traffic['peak_up']); + $line = function (string $key) use ($pts, $n, $peak) { + $out = []; + foreach ($pts as $i => $p) { + $x = round($i / $n * 600, 1); + $y = round(140 - ($p[$key] / $peak) * 132, 1); + $out[] = $x.','.$y; + } + return implode(' ', $out); + }; + $hasTraffic = $traffic['total_down'] > 0 || $traffic['total_up'] > 0; + @endphp + + +
+
+ {{ __('wireguard.total_down') }}: {{ $fmtBytes($traffic['total_down']) }} + {{ __('wireguard.total_up') }}: {{ $fmtBytes($traffic['total_up']) }} +
+
+ @foreach ($windows as $w) + + @endforeach +
+
+
+ @if ($hasTraffic) + + + + + @else +

{{ __('wireguard.no_traffic') }}

+ @endif +
+
+ + +
+ + + {{ __('wireguard.add_peer') }} + + @error('newPeer') {{ $message }} @enderror +
+
+ @forelse ($status['peers'] as $peer) +
+ + + {{ $peer['name'] !== '' ? $peer['name'] : \Illuminate\Support\Str::limit($peer['pubkey'], 10, '…') }} + + {{ $peer['online'] ? __('wireguard.online') : __('wireguard.offline') }} + {{ __('wireguard.last_handshake') }}: {{ $ago($peer['latest_handshake']) }} + + {{ $peer['endpoint'] !== '' ? $peer['endpoint'] : '—' }} + {{ __('wireguard.down_up', ['rx' => $fmtBytes($peer['rx']), 'tx' => $fmtBytes($peer['tx'])]) }} + +
+ + {{ __('wireguard.remove') }} + +
+
+ @empty +

{{ __('wireguard.no_peers') }}

+ @endforelse +
+
+ @endif @endif @if ($resultConfig) diff --git a/tests/Feature/WireguardPageTest.php b/tests/Feature/WireguardPageTest.php index 30c2dd6..617577e 100644 --- a/tests/Feature/WireguardPageTest.php +++ b/tests/Feature/WireguardPageTest.php @@ -52,8 +52,8 @@ class WireguardPageTest extends TestCase Livewire::test(Index::class) ->assertOk() ->assertViewHas('status', fn ($s) => $s['configured'] === true && count($s['peers']) === 1) - ->assertSee('laptop') - ->assertSee('10.99.0.0/24'); + ->assertSee('laptop') // peers tab (default) + ->set('tab', 'server')->assertSee('10.99.0.0/24'); // server tab shows the subnet } public function test_shows_ssh_lock_active_state_when_the_host_reports_it(): void @@ -67,6 +67,7 @@ class WireguardPageTest extends TestCase Livewire::test(Index::class) ->assertViewHas('status', fn ($s) => $s['gate']['ssh'] === true) + ->set('tab', 'server') // SSH lock lives in the server-configuration tab ->assertSee(__('wireguard.ssh_lock')) ->assertSee(__('wireguard.ssh_lock_turn_off')); } @@ -187,6 +188,49 @@ class WireguardPageTest extends TestCase @unlink(storage_path("app/restart-signal/wg-result-{$id}.json")); } + public function test_set_dns_writes_a_request_and_audits(): void + { + Livewire::test(Index::class) + ->set('newDns', '10.0.0.1, 1.1.1.1') + ->call('setDns') + ->assertSet('pendingAction', 'set-dns') + ->assertSet('pendingId', fn ($v) => is_string($v) && $v !== ''); + $this->assertTrue(AuditEvent::where('action', 'wg.set-dns')->exists()); + @unlink(storage_path('app/restart-signal/wg-request.json')); + } + + public function test_set_dns_rejects_a_bad_value(): void + { + Livewire::test(Index::class) + ->set('newDns', 'not-an-ip') + ->call('setDns') + ->assertHasErrors('newDns'); + $this->assertSame(0, AuditEvent::where('action', 'wg.set-dns')->count()); + } + + public function test_set_tab_switches_and_clamps_unknown_values(): void + { + Livewire::test(Index::class) + ->assertSet('tab', 'peers') + ->call('setTab', 'server')->assertSet('tab', 'server') + ->call('setTab', 'bogus')->assertSet('tab', 'peers'); + } + + public function test_status_exposes_the_server_dns(): void + { + file_put_contents($this->file, json_encode([ + 'at' => time(), 'configured' => true, + 'gate' => ['marker' => true, 'chain' => true, 'ssh' => false], 'wg_quick' => 'active', + 'server' => ['subnet' => '10.99.0.0/24', 'server_ip' => '10.99.0.1', 'port' => 51820, 'endpoint' => '1.2.3.4:51820', 'dns' => '10.0.0.1', 'pubkey' => 'srvpub'], + 'peers' => [], + ])); + + Livewire::test(Index::class) + ->set('tab', 'server') + ->assertViewHas('status', fn ($s) => $s['server']['dns'] === '10.0.0.1') + ->assertSee('10.0.0.1'); + } + public function test_setup_writes_a_request_and_audits(): void { Livewire::test(Index::class)