refactor(wireguard): remove dead code + DRY the WG surface (no behavior change)
Verified dead-code + duplication cleanup over the WireGuard feature surface (found via an audit workflow, every item adversarially re-verified by grep): Dead code removed: - lang keys not_configured_title / not_configured_body (both locales) — orphaned when the unconfigured empty state became the in-page setup form (setup_title / setup_intro). 0 references anywhere. de/en parity kept (83/83). - WgTraffic::series() 'buckets' return key — written, never read by blade/route/test. - blade $fmtB byte-formatter — byte-identical duplicate of the top-level $fmtBytes already in scope; collapsed onto $fmtBytes. DRY / readability (behavior-preserving): - Index: PEER_NAME_RE constant (was the same regex literal at 5 sites), portInRange() + clampWindow() helpers (window default now self::WINDOWS[0]), openConfirm() helper (5 confirm-modal openers shared one dispatch), onFlag() helper (2 apply handlers). - WgBridge: validPort() helper (port check was duplicated verbatim). - clusev-wg.sh: _write_server_conf() shared by both setup paths (the byte-identical server bring-up was copy-pasted); single CIDR_RE constant for the 3 subnet checks; fixed a stale header comment that still claimed SSH is "NEVER matched" (the v0.9.40 ssh-lock can match 22); documented the defence-in-depth re-validation in _set_*. - WgStatus: documented server.pubkey is part of the /wg-status.json contract (kept). No DB/migration changes (peer_name write-only column left intact — needs a coordinated migration). 348 tests pass, shellcheck clean, Pint clean, R12 verified in both the configured + unconfigured states (200, no console errors, no leaked keys, traffic formatter renders 1.5 MB / 200 KB correctly). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/v1-foundation
parent
2b245c3d3a
commit
9c0d390365
|
|
@ -22,10 +22,13 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
|||
#[Layout('layouts.app')]
|
||||
class Index extends Component
|
||||
{
|
||||
public int $window = 3600;
|
||||
|
||||
public const WINDOWS = [3600, 86400, 604800];
|
||||
|
||||
/** Peer-name charset, shared by every app-side validation site (the host re-validates too). */
|
||||
private const PEER_NAME_RE = '/^[A-Za-z0-9._-]{1,64}$/';
|
||||
|
||||
public int $window = self::WINDOWS[0];
|
||||
|
||||
public string $newPeer = '';
|
||||
|
||||
public string $newEndpoint = '';
|
||||
|
|
@ -58,12 +61,12 @@ class Index extends Component
|
|||
|
||||
public function setWindow(int $seconds): void
|
||||
{
|
||||
$this->window = in_array($seconds, self::WINDOWS, true) ? $seconds : 3600;
|
||||
$this->window = $this->clampWindow($seconds);
|
||||
}
|
||||
|
||||
public function addPeer(WgBridge $bridge): void
|
||||
{
|
||||
$this->validate(['newPeer' => ['required', 'regex:/^[A-Za-z0-9._-]{1,64}$/']], [
|
||||
$this->validate(['newPeer' => ['required', 'regex:'.self::PEER_NAME_RE]], [
|
||||
'newPeer.regex' => __('wireguard.peer_name_invalid'),
|
||||
'newPeer.required' => __('wireguard.peer_name_invalid'),
|
||||
]);
|
||||
|
|
@ -83,14 +86,14 @@ class Index extends Component
|
|||
'setupSubnet' => ['required', 'regex:#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#'],
|
||||
'setupPort' => ['required', 'regex:/^\d{1,5}$/'],
|
||||
'setupEndpoint' => ['nullable', 'regex:/^[A-Za-z0-9.:_-]{1,128}$/'],
|
||||
'setupPeer' => ['required', 'regex:/^[A-Za-z0-9._-]{1,64}$/'],
|
||||
'setupPeer' => ['required', 'regex:'.self::PEER_NAME_RE],
|
||||
], [
|
||||
'setupSubnet.regex' => __('wireguard.subnet_invalid'),
|
||||
'setupPort.regex' => __('wireguard.port_invalid'),
|
||||
'setupEndpoint.regex' => __('wireguard.endpoint_invalid'),
|
||||
'setupPeer.regex' => __('wireguard.peer_name_invalid'),
|
||||
]);
|
||||
if ((int) $this->setupPort < 1 || (int) $this->setupPort > 65535) {
|
||||
if (! $this->portInRange($this->setupPort)) {
|
||||
$this->addError('setupPort', __('wireguard.port_invalid'));
|
||||
|
||||
return;
|
||||
|
|
@ -109,24 +112,13 @@ class Index extends Component
|
|||
/** Opens the wire-elements/modal confirm dialog for peer removal. */
|
||||
public function confirmRemovePeer(string $name): void
|
||||
{
|
||||
if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) {
|
||||
if (preg_match(self::PEER_NAME_RE, $name) !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatch('openModal',
|
||||
component: 'modals.confirm-action',
|
||||
arguments: [
|
||||
'heading' => __('wireguard.remove_confirm_title'),
|
||||
'body' => __('wireguard.remove_confirm_body'),
|
||||
'confirmLabel' => __('wireguard.remove'),
|
||||
'danger' => true,
|
||||
'icon' => 'trash',
|
||||
'notify' => '',
|
||||
// No audit descriptor — the apply path (removePeer) audits, so the confirm modal
|
||||
// must NOT also audit (avoids a double audit row for one action).
|
||||
'token' => ConfirmToken::issue('wgPeerRemoved', ['name' => $name]),
|
||||
],
|
||||
);
|
||||
$this->openConfirm('wgPeerRemoved', ['name' => $name],
|
||||
__('wireguard.remove_confirm_title'), __('wireguard.remove_confirm_body'),
|
||||
__('wireguard.remove'), danger: true, icon: 'trash');
|
||||
}
|
||||
|
||||
/** Apply handler — called after the ConfirmAction modal confirms the removal. */
|
||||
|
|
@ -140,7 +132,7 @@ class Index extends Component
|
|||
}
|
||||
|
||||
$name = $payload['params']['name'] ?? '';
|
||||
if ($name === '' || preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) {
|
||||
if ($name === '' || preg_match(self::PEER_NAME_RE, $name) !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +141,7 @@ class Index extends Component
|
|||
|
||||
public function removePeer(WgBridge $bridge, string $name): void
|
||||
{
|
||||
if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1 || ! $this->throttle()) {
|
||||
if (preg_match(self::PEER_NAME_RE, $name) !== 1 || ! $this->throttle()) {
|
||||
return;
|
||||
}
|
||||
$this->pendingId = $bridge->request('remove-peer', ['name' => $name]);
|
||||
|
|
@ -174,18 +166,11 @@ class Index extends Component
|
|||
|
||||
public function confirmGate(bool $on): void
|
||||
{
|
||||
$this->dispatch('openModal',
|
||||
component: 'modals.confirm-action',
|
||||
arguments: [
|
||||
'heading' => $on ? __('wireguard.gate_on_title') : __('wireguard.gate_off_title'),
|
||||
'body' => $on ? __('wireguard.gate_on_body') : __('wireguard.gate_off_body'),
|
||||
'confirmLabel' => $on ? __('wireguard.gate_turn_on') : __('wireguard.gate_turn_off'),
|
||||
'danger' => ! $on,
|
||||
'icon' => 'shield',
|
||||
'notify' => '',
|
||||
'token' => ConfirmToken::issue('wgGateToggle', ['on' => $on ? '1' : '0']),
|
||||
],
|
||||
);
|
||||
$this->openConfirm('wgGateToggle', ['on' => $on ? '1' : '0'],
|
||||
$on ? __('wireguard.gate_on_title') : __('wireguard.gate_off_title'),
|
||||
$on ? __('wireguard.gate_on_body') : __('wireguard.gate_off_body'),
|
||||
$on ? __('wireguard.gate_turn_on') : __('wireguard.gate_turn_off'),
|
||||
danger: ! $on, icon: 'shield');
|
||||
}
|
||||
|
||||
#[On('wgGateToggle')]
|
||||
|
|
@ -197,7 +182,7 @@ class Index extends Component
|
|||
return;
|
||||
}
|
||||
|
||||
$this->runGate(($payload['params']['on'] ?? '0') === '1', $bridge);
|
||||
$this->runGate($this->onFlag($payload), $bridge);
|
||||
}
|
||||
|
||||
public function runGate(bool $on, ?WgBridge $bridge = null): void
|
||||
|
|
@ -218,18 +203,11 @@ class Index extends Component
|
|||
*/
|
||||
public function confirmSshGate(bool $on): void
|
||||
{
|
||||
$this->dispatch('openModal',
|
||||
component: 'modals.confirm-action',
|
||||
arguments: [
|
||||
'heading' => $on ? __('wireguard.ssh_lock_on_title') : __('wireguard.ssh_lock_off_title'),
|
||||
'body' => $on ? __('wireguard.ssh_lock_on_body') : __('wireguard.ssh_lock_off_body'),
|
||||
'confirmLabel' => $on ? __('wireguard.ssh_lock_turn_on') : __('wireguard.ssh_lock_turn_off'),
|
||||
'danger' => $on,
|
||||
'icon' => 'alert',
|
||||
'notify' => '',
|
||||
'token' => ConfirmToken::issue('wgSshGate', ['on' => $on ? '1' : '0']),
|
||||
],
|
||||
);
|
||||
$this->openConfirm('wgSshGate', ['on' => $on ? '1' : '0'],
|
||||
$on ? __('wireguard.ssh_lock_on_title') : __('wireguard.ssh_lock_off_title'),
|
||||
$on ? __('wireguard.ssh_lock_on_body') : __('wireguard.ssh_lock_off_body'),
|
||||
$on ? __('wireguard.ssh_lock_turn_on') : __('wireguard.ssh_lock_turn_off'),
|
||||
danger: $on, icon: 'alert');
|
||||
}
|
||||
|
||||
#[On('wgSshGate')]
|
||||
|
|
@ -241,7 +219,7 @@ class Index extends Component
|
|||
return;
|
||||
}
|
||||
|
||||
$this->runSshGate(($payload['params']['on'] ?? '0') === '1', $bridge);
|
||||
$this->runSshGate($this->onFlag($payload), $bridge);
|
||||
}
|
||||
|
||||
public function runSshGate(bool $on, ?WgBridge $bridge = null): void
|
||||
|
|
@ -261,24 +239,15 @@ class Index extends Component
|
|||
['newPort' => ['required', 'regex:/^\d{1,5}$/']],
|
||||
['newPort.regex' => __('wireguard.port_invalid'), 'newPort.required' => __('wireguard.port_invalid')],
|
||||
);
|
||||
if ((int) $this->newPort < 1 || (int) $this->newPort > 65535) {
|
||||
if (! $this->portInRange($this->newPort)) {
|
||||
$this->addError('newPort', __('wireguard.port_invalid'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatch('openModal',
|
||||
component: 'modals.confirm-action',
|
||||
arguments: [
|
||||
'heading' => __('wireguard.port_confirm_title'),
|
||||
'body' => __('wireguard.port_confirm_body'),
|
||||
'confirmLabel' => __('wireguard.apply'),
|
||||
'danger' => true,
|
||||
'icon' => 'alert',
|
||||
'notify' => '',
|
||||
'token' => ConfirmToken::issue('wgSetPort', ['port' => $this->newPort]),
|
||||
],
|
||||
);
|
||||
$this->openConfirm('wgSetPort', ['port' => $this->newPort],
|
||||
__('wireguard.port_confirm_title'), __('wireguard.port_confirm_body'),
|
||||
__('wireguard.apply'), danger: true, icon: 'alert');
|
||||
}
|
||||
|
||||
#[On('wgSetPort')]
|
||||
|
|
@ -307,18 +276,9 @@ class Index extends Component
|
|||
['newSubnet.regex' => __('wireguard.subnet_invalid'), 'newSubnet.required' => __('wireguard.subnet_invalid')],
|
||||
);
|
||||
|
||||
$this->dispatch('openModal',
|
||||
component: 'modals.confirm-action',
|
||||
arguments: [
|
||||
'heading' => __('wireguard.subnet_confirm_title'),
|
||||
'body' => __('wireguard.subnet_confirm_body'),
|
||||
'confirmLabel' => __('wireguard.apply'),
|
||||
'danger' => true,
|
||||
'icon' => 'alert',
|
||||
'notify' => '',
|
||||
'token' => ConfirmToken::issue('wgSetSubnet', ['subnet' => $this->newSubnet]),
|
||||
],
|
||||
);
|
||||
$this->openConfirm('wgSetSubnet', ['subnet' => $this->newSubnet],
|
||||
__('wireguard.subnet_confirm_title'), __('wireguard.subnet_confirm_body'),
|
||||
__('wireguard.apply'), danger: true, icon: 'alert');
|
||||
}
|
||||
|
||||
#[On('wgSetSubnet')]
|
||||
|
|
@ -392,6 +352,47 @@ class Index extends Component
|
|||
}, 'clusev-wireguard.conf', ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||
}
|
||||
|
||||
/** Clamp a traffic window to one of the allowed values, falling back to the shortest. */
|
||||
private function clampWindow(int $seconds): int
|
||||
{
|
||||
return in_array($seconds, self::WINDOWS, true) ? $seconds : self::WINDOWS[0];
|
||||
}
|
||||
|
||||
/** True when a numeric-string port is within the valid UDP range. */
|
||||
private function portInRange(string $port): bool
|
||||
{
|
||||
return (int) $port >= 1 && (int) $port <= 65535;
|
||||
}
|
||||
|
||||
/** Decode the sealed on/off flag a gate-toggle confirm token carries. */
|
||||
private function onFlag(array $payload): bool
|
||||
{
|
||||
return ($payload['params']['on'] ?? '0') === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the shared ConfirmAction modal (R5) for one WG action. The issued token carries NO audit
|
||||
* descriptor on purpose — each apply handler audits exactly once itself, so auditing here too
|
||||
* would double-count. Heading/body/label/danger/icon/params are the only per-action differences.
|
||||
*
|
||||
* @param array<string, string> $params
|
||||
*/
|
||||
private function openConfirm(string $event, array $params, string $heading, string $body, string $confirmLabel, bool $danger, string $icon): void
|
||||
{
|
||||
$this->dispatch('openModal',
|
||||
component: 'modals.confirm-action',
|
||||
arguments: [
|
||||
'heading' => $heading,
|
||||
'body' => $body,
|
||||
'confirmLabel' => $confirmLabel,
|
||||
'danger' => $danger,
|
||||
'icon' => $icon,
|
||||
'notify' => '',
|
||||
'token' => ConfirmToken::issue($event, $params),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function throttle(): bool
|
||||
{
|
||||
$key = 'wg-request:'.Auth::id();
|
||||
|
|
@ -418,7 +419,7 @@ class Index extends Component
|
|||
|
||||
public function render(WgStatus $wg, WgTraffic $traffic)
|
||||
{
|
||||
$window = in_array($this->window, self::WINDOWS, true) ? $this->window : 3600;
|
||||
$window = $this->clampWindow($this->window);
|
||||
|
||||
return view('livewire.wireguard.index', [
|
||||
'status' => $wg->read(),
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class WgBridge
|
|||
return ['endpoint' => $ep];
|
||||
case 'set-port':
|
||||
$port = (string) ($args['port'] ?? '');
|
||||
if (preg_match('/^\d{1,5}$/', $port) !== 1 || (int) $port < 1 || (int) $port > 65535) {
|
||||
if (! $this->validPort($port)) {
|
||||
throw new \InvalidArgumentException('invalid port');
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ class WgBridge
|
|||
if (preg_match('#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#', $subnet) !== 1) {
|
||||
throw new \InvalidArgumentException('invalid subnet');
|
||||
}
|
||||
if (preg_match('/^\d{1,5}$/', $port) !== 1 || (int) $port < 1 || (int) $port > 65535) {
|
||||
if (! $this->validPort($port)) {
|
||||
throw new \InvalidArgumentException('invalid port');
|
||||
}
|
||||
if ($endpoint !== '' && preg_match('/^[A-Za-z0-9.:_-]{1,128}$/', $endpoint) !== 1) {
|
||||
|
|
@ -135,4 +135,10 @@ class WgBridge
|
|||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** A valid 1–65535 UDP port given as a numeric string. */
|
||||
private function validPort(string $port): bool
|
||||
{
|
||||
return preg_match('/^\d{1,5}$/', $port) === 1 && (int) $port >= 1 && (int) $port <= 65535;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class WgStatus
|
|||
'server_ip' => (string) ($data['server']['server_ip'] ?? ''),
|
||||
'port' => (int) ($data['server']['port'] ?? 0),
|
||||
'endpoint' => (string) ($data['server']['endpoint'] ?? ''),
|
||||
// Not rendered by the page, but part of the /wg-status.json response contract (API consumers).
|
||||
'pubkey' => (string) ($data['server']['pubkey'] ?? ''),
|
||||
],
|
||||
'peers' => $peers,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use App\Models\WgTrafficSample;
|
|||
class WgTraffic
|
||||
{
|
||||
/**
|
||||
* @return array{window:int, buckets:int, points:array<int,array{t:int,down:int,up:int}>,
|
||||
* @return array{window:int, points:array<int,array{t:int,down:int,up:int}>,
|
||||
* total_down:int, total_up:int, peak_down:int, peak_up:int}
|
||||
*/
|
||||
public function series(int $windowSeconds, int $buckets = 60): array
|
||||
|
|
@ -59,7 +59,6 @@ class WgTraffic
|
|||
|
||||
return [
|
||||
'window' => $windowSeconds,
|
||||
'buckets' => $buckets,
|
||||
'points' => $points,
|
||||
'total_down' => array_sum($down),
|
||||
'total_up' => array_sum($up),
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
# Clusev WireGuard gate + peer CLI (HOST-side, root). Invoked via `clusev wg <cmd>` (the host
|
||||
# wrapper execs this from the deployed repo tree). Puts the panel behind a WG tunnel: the VM is the
|
||||
# WG server, operator devices peer in, and TCP 80/443 is filtered to the WG subnet by a dedicated
|
||||
# iptables chain hung off DOCKER-USER. SSH (22) and the WG UDP port are NEVER matched — you can
|
||||
# always SSH in and run `clusev wg down`. The gate is OFF until `clusev wg up`.
|
||||
# iptables chain hung off DOCKER-USER. The WG UDP port is NEVER matched. SSH (22) is only filtered if
|
||||
# the operator opts into the SEPARATE ssh-lock (default off); the server console + `clusev wg down`
|
||||
# are always a way back in. The panel gate is OFF until `clusev wg up`.
|
||||
set -euo pipefail
|
||||
|
||||
WG_IF="wg0"
|
||||
|
|
@ -20,6 +21,8 @@ PANEL_PORTS="80,443" # see spec §3 post-DNAT note: matches the published cont
|
|||
# current SSH session, only NEW non-tunnel SSH. Off unless the operator explicitly enables it.
|
||||
SSH_MARKER="${STATE_DIR}/wg-ssh-gate.enabled"
|
||||
SSH_CHAIN="CLUSEV-WG-SSH"
|
||||
# Shape-check for a /N CIDR (single source for the subnet validators below). Single-quoted = literal.
|
||||
readonly CIDR_RE='^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$'
|
||||
|
||||
# Project root (for the run/ bridge dir). The collect timer sets CLUSEV_DIR; otherwise derive it
|
||||
# from this script's location (docker/wg/clusev-wg.sh -> ../..).
|
||||
|
|
@ -102,7 +105,7 @@ ssh_gate_apply() {
|
|||
[ -n "${WG_SUBNET:-}" ] || return 0
|
||||
# Re-validate the subnet shape before it reaches `iptables -s`; a malformed value fails open
|
||||
# (no DROP) rather than aborting mid-chain and leaving SSH half-filtered.
|
||||
printf '%s' "$WG_SUBNET" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$' || return 0
|
||||
printf '%s' "$WG_SUBNET" | grep -qE "$CIDR_RE" || return 0
|
||||
iptables -N "$SSH_CHAIN" 2>/dev/null || iptables -F "$SSH_CHAIN"
|
||||
iptables -A "$SSH_CHAIN" -i lo -j RETURN
|
||||
iptables -A "$SSH_CHAIN" -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN # never cut a live session
|
||||
|
|
@ -297,7 +300,7 @@ _setup_noninteractive() {
|
|||
local subnet="$1" server_ip="$2" port="$3" endpoint="$4" peer1="$5"
|
||||
have wg || { echo "wireguard-tools fehlt" >&2; return 1; }
|
||||
[ -f "$WG_CONF" ] && { echo "WireGuard ist bereits eingerichtet" >&2; return 1; }
|
||||
printf '%s' "$subnet" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$' || { echo "Ungueltiges Subnetz" >&2; return 1; }
|
||||
printf '%s' "$subnet" | grep -qE "$CIDR_RE" || { echo "Ungueltiges Subnetz" >&2; return 1; }
|
||||
case "$server_ip" in ''|*[!0-9.]*) echo "Ungueltige Server-IP" >&2; return 1 ;; esac
|
||||
case "$port" in ''|*[!0-9]*) echo "Ungueltiger Port" >&2; return 1 ;; esac
|
||||
[ "$port" -ge 1 ] && [ "$port" -le 65535 ] || { echo "Port ausserhalb 1-65535" >&2; return 1; }
|
||||
|
|
@ -305,6 +308,16 @@ _setup_noninteractive() {
|
|||
valid_name "$peer1" || { echo "Ungueltiger Peer-Name" >&2; return 1; }
|
||||
subnet_collides "$subnet" && { echo "Subnetz kollidiert mit einer bestehenden Route/Adresse" >&2; return 1; }
|
||||
|
||||
_write_server_conf "$subnet" "$server_ip" "$port" "$endpoint"
|
||||
systemctl enable --now "wg-quick@${WG_IF}" || { echo "wg-quick@${WG_IF} konnte nicht gestartet werden" >&2; return 1; }
|
||||
_peer_add "$peer1"
|
||||
}
|
||||
|
||||
# Generate the server keypair and write wg0.conf + wg.env (both 0600). Shared by both setup paths
|
||||
# (dashboard _setup_noninteractive + interactive cmd_setup); the caller does `systemctl enable --now`
|
||||
# and the first-peer add, since those differ (error style return-vs-die, _peer_add vs cmd_add_peer).
|
||||
_write_server_conf() {
|
||||
local subnet="$1" server_ip="$2" port="$3" endpoint="$4"
|
||||
umask 077; mkdir -p "$WG_DIR" "$STATE_DIR"
|
||||
local srv_priv srv_pub prefix
|
||||
srv_priv="$(wg genkey)"; srv_pub="$(printf '%s' "$srv_priv" | wg pubkey)"
|
||||
|
|
@ -324,8 +337,6 @@ WG_ENDPOINT=${endpoint}
|
|||
WG_SERVER_PUBKEY=${srv_pub}
|
||||
EOF
|
||||
chmod 600 "$WG_ENV"
|
||||
systemctl enable --now "wg-quick@${WG_IF}" || { echo "wg-quick@${WG_IF} konnte nicht gestartet werden" >&2; return 1; }
|
||||
_peer_add "$peer1"
|
||||
}
|
||||
|
||||
cmd_setup() {
|
||||
|
|
@ -368,28 +379,7 @@ cmd_setup() {
|
|||
[ -n "$endpoint" ] || die "Endpoint erforderlich."
|
||||
read -rp " Name des ersten Peers [client-1]: " peer1 || true; peer1="${peer1:-client-1}"
|
||||
|
||||
umask 077; mkdir -p "$WG_DIR" "$STATE_DIR"
|
||||
local srv_priv srv_pub prefix
|
||||
srv_priv="$(wg genkey)"; srv_pub="$(printf '%s' "$srv_priv" | wg pubkey)"
|
||||
prefix="${subnet##*/}"
|
||||
|
||||
cat > "$WG_CONF" <<EOF
|
||||
[Interface]
|
||||
Address = ${server_ip}/${prefix}
|
||||
ListenPort = ${port}
|
||||
PrivateKey = ${srv_priv}
|
||||
EOF
|
||||
chmod 600 "$WG_CONF"
|
||||
|
||||
cat > "$WG_ENV" <<EOF
|
||||
WG_SUBNET=${subnet}
|
||||
WG_SERVER_IP=${server_ip}
|
||||
WG_PORT=${port}
|
||||
WG_ENDPOINT=${endpoint}
|
||||
WG_SERVER_PUBKEY=${srv_pub}
|
||||
EOF
|
||||
chmod 600 "$WG_ENV"
|
||||
|
||||
_write_server_conf "$subnet" "$server_ip" "$port" "$endpoint"
|
||||
systemctl enable --now "wg-quick@${WG_IF}" || die "wg-quick@${WG_IF} konnte nicht gestartet werden — 'journalctl -u wg-quick@${WG_IF}' pruefen."
|
||||
cmd_add_peer "$peer1"
|
||||
echo
|
||||
|
|
@ -491,12 +481,14 @@ cmd_serve_request() {
|
|||
# ── settings mutators (cores: no prints; errors to stderr; non-zero on fail) ──────────────────
|
||||
_set_endpoint() {
|
||||
local ep="$1"; require_setup
|
||||
# Re-validated here (defence-in-depth): the bare CLI path cmd_set_endpoint passes $1 unchecked.
|
||||
case "$ep" in *[!A-Za-z0-9.:_-]*|'') echo "bad endpoint" >&2; return 1 ;; esac
|
||||
sed -i "s#^WG_ENDPOINT=.*#WG_ENDPOINT=${ep}#" "$WG_ENV"
|
||||
}
|
||||
|
||||
_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.
|
||||
case "$port" in ''|*[!0-9]*) echo "bad port" >&2; return 1 ;; esac
|
||||
[ "$port" -ge 1 ] && [ "$port" -le 65535 ] || { echo "port out of range" >&2; return 1; }
|
||||
sed -i "s#^ListenPort = .*#ListenPort = ${port}#" "$WG_CONF"
|
||||
|
|
@ -506,8 +498,9 @@ _set_port() {
|
|||
|
||||
_set_subnet() {
|
||||
local subnet="$1"; require_setup
|
||||
# Re-validated here (defence-in-depth): the bare CLI path cmd_set_subnet passes $1 unchecked.
|
||||
case "$subnet" in *[!0-9./]*|'') echo "bad subnet" >&2; return 1 ;; esac
|
||||
printf '%s' "$subnet" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$' || { echo "bad subnet" >&2; return 1; }
|
||||
printf '%s' "$subnet" | grep -qE "$CIDR_RE" || { echo "bad subnet" >&2; return 1; }
|
||||
subnet_collides "$subnet" && { echo "subnet collides with an existing route/address" >&2; return 1; }
|
||||
local prefix server_ip
|
||||
prefix="${subnet##*/}"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ return [
|
|||
'eyebrow' => 'Netzwerk',
|
||||
'heading' => 'WireGuard',
|
||||
'subtitle' => 'Tunnel-Zugang zum Panel — Live-Status.',
|
||||
'not_configured_title' => 'WireGuard ist nicht eingerichtet',
|
||||
'not_configured_body' => 'Auf dem Host einrichten: per SSH "sudo clusev wg setup". Danach erscheint hier der Live-Status.',
|
||||
'stale' => 'Status veraltet — der Host-Collector antwortet nicht. Läuft der Stack?',
|
||||
'gate' => 'Gate (Panel-Sperre)',
|
||||
'gate_on' => 'aktiv — Panel nur über den Tunnel',
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ return [
|
|||
'eyebrow' => 'Network',
|
||||
'heading' => 'WireGuard',
|
||||
'subtitle' => 'Tunnel access to the panel — live status.',
|
||||
'not_configured_title' => 'WireGuard is not set up',
|
||||
'not_configured_body' => 'Set it up on the host: over SSH run "sudo clusev wg setup". The live status appears here afterwards.',
|
||||
'stale' => 'Status is stale — the host collector is not responding. Is the stack running?',
|
||||
'gate' => 'Gate (panel lock)',
|
||||
'gate_on' => 'on — panel only via the tunnel',
|
||||
|
|
|
|||
|
|
@ -79,11 +79,6 @@
|
|||
$win = collect($windows)->mapWithKeys(fn ($s) => [$s => match ($s) {
|
||||
3600 => __('wireguard.window_1h'), 86400 => __('wireguard.window_24h'), default => __('wireguard.window_7d'),
|
||||
}])->all();
|
||||
$fmtB = function (int $n): string {
|
||||
$u = ['B', 'KB', 'MB', 'GB', 'TB']; $i = 0; $v = (float) $n;
|
||||
while ($v >= 1024 && $i < count($u) - 1) { $v /= 1024; $i++; }
|
||||
return ($i === 0 ? (string) $n : number_format($v, 1)).' '.$u[$i];
|
||||
};
|
||||
$pts = $traffic['points'];
|
||||
$n = max(1, count($pts) - 1);
|
||||
$peak = max(1, $traffic['peak_down'], $traffic['peak_up']);
|
||||
|
|
@ -102,8 +97,8 @@
|
|||
<x-panel :title="__('wireguard.traffic_title')" :padded="false">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-4 py-3 sm:px-5">
|
||||
<div class="flex items-center gap-4 font-mono text-[11px]">
|
||||
<span class="text-accent-text">{{ __('wireguard.total_down') }}: <span class="text-ink-2">{{ $fmtB($traffic['total_down']) }}</span></span>
|
||||
<span class="text-cyan">{{ __('wireguard.total_up') }}: <span class="text-ink-2">{{ $fmtB($traffic['total_up']) }}</span></span>
|
||||
<span class="text-accent-text">{{ __('wireguard.total_down') }}: <span class="text-ink-2">{{ $fmtBytes($traffic['total_down']) }}</span></span>
|
||||
<span class="text-cyan">{{ __('wireguard.total_up') }}: <span class="text-ink-2">{{ $fmtBytes($traffic['total_up']) }}</span></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
@foreach ($windows as $w)
|
||||
|
|
|
|||
Loading…
Reference in New Issue