alle Jails * 'recidive' => nur dieses Jail * 'mailwolt-blacklist' etc. */ public ?string $jail = null; /** * @var array */ public array $rows = []; #[On('f2b:refresh')] public function refreshList(): void { $this->loadBanned(); } public function mount(?string $jail = null): void { $this->jail = $jail; $this->loadBanned(); } public function render() { return view('livewire.ui.security.fail2ban-banlist'); } /* ================= core ================= */ private function loadBanned(): void { $jails = $this->jailList(); // ggf. nur ein bestimmtes Jail if (is_string($this->jail) && $this->jail !== '' && $this->jail !== '*') { $jails = in_array($this->jail, $jails, true) ? [$this->jail] : []; } $rows = []; foreach ($jails as $j) { $out = $this->f2b("status " . escapeshellarg($j)); if (!preg_match('/IP list:\s*(.+)$/mi', $out, $m)) { continue; } $ips = preg_split('/\s+/', trim($m[1])) ?: []; foreach ($ips as $ip) { if (!filter_var($ip, FILTER_VALIDATE_IP)) { continue; } $banInfo = $this->getBanInfo($j, $ip); $permanent = $banInfo['permanent']; $remaining = $banInfo['remaining']; if ($permanent) { $box = 'border-rose-400/30 bg-rose-500/5'; $badge = 'border-rose-400/30 bg-rose-500/10 text-rose-200'; $label = 'Permanent'; $style = 'permanent'; $dot = 'bg-rose-500'; } else { $box = 'border-amber-400/20 bg-white/3'; $badge = 'border-amber-400/30 bg-amber-500/10 text-amber-200'; $label = 'Temporär'; $style = 'temporary'; $dot = 'bg-amber-400'; } $rows[] = [ 'ip' => $ip, 'jail' => $j, 'service' => $this->serviceLabel($j), 'permanent' => $permanent, 'style' => $style, 'label' => $label, 'remaining' => $remaining, 'box' => $box, 'badge' => $badge, 'dot' => $dot, 'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50', ]; } } // Sortierung: permanent oben, dann nach Jail, dann IP usort($rows, function ($a, $b) { if ($a['permanent'] !== $b['permanent']) return $a['permanent'] ? -1 : 1; if ($a['jail'] !== $b['jail']) return strcmp($a['jail'], $b['jail']); return strcmp($a['ip'], $b['ip']); }); $this->rows = $rows; } /** Entbannt eine IP **im angegebenen Jail** (Button gibt Jail mit) */ public function unban(string $ip, string $jail): void { if (!filter_var($ip, FILTER_VALIDATE_IP)) return; $cmd = sprintf( 'sudo -n /usr/bin/fail2ban-client set %s unbanip %s 2>&1', escapeshellarg($jail), escapeshellarg($ip) ); @shell_exec($cmd); $this->loadBanned(); $this->dispatch('toast', type: 'done', badge: 'Fail2Ban', title: 'IP entbannt', text: "IP {$ip} in Jail „{$jail}“ entbannt.", duration: 5000, ); } /* ================= helpers ================= */ /** Gibt permanent-Flag und verbleibende Zeit für (jail, ip) zurück. */ private function getBanInfo(string $jail, string $ip): array { $db = $this->getDbFile(); $fallbackPermanent = ($jail === 'mailwolt-blacklist'); if ($db === '') { return ['permanent' => $fallbackPermanent, 'remaining' => '']; } $q = sprintf( "WITH last AS (SELECT MAX(timeofban) AS t FROM bans WHERE jail='%s' AND ip='%s') ". "SELECT timeofban, bantime FROM bans, last ". "WHERE jail='%s' AND ip='%s' AND timeofban=last.t LIMIT 1;", SQLite3::escapeString($jail), SQLite3::escapeString($ip), SQLite3::escapeString($jail), SQLite3::escapeString($ip) ); $cmd = sprintf('sudo -n /usr/bin/sqlite3 -readonly %s %s 2>&1', escapeshellarg($db), escapeshellarg($q)); $out = trim((string)@shell_exec($cmd)); if ($out === '') { return ['permanent' => $fallbackPermanent, 'remaining' => '']; } [$timeofban, $bantime] = array_pad(explode('|', $out), 2, '0'); $timeofban = (int)$timeofban; $bantime = (int)$bantime; if ($bantime < 0) { return ['permanent' => true, 'remaining' => '']; } $remaining = max(0, ($timeofban + $bantime) - time()); return [ 'permanent' => false, 'remaining' => $this->formatRemaining($remaining), ]; } private function formatRemaining(int $seconds): string { if ($seconds <= 0) return 'läuft ab'; if ($seconds < 60) return "noch {$seconds} Sek."; if ($seconds < 3600) return 'noch ' . (int)ceil($seconds / 60) . ' Min.'; if ($seconds < 86400) return 'noch ' . round($seconds / 3600, 1) . ' Std.'; return 'noch ' . (int)ceil($seconds / 86400) . ' Tage'; } private function serviceLabel(string $jail): string { return match(true) { $jail === 'sshd' => 'SSH', $jail === 'dovecot' => 'IMAP/POP3', str_starts_with($jail, 'postfix') => 'SMTP', str_starts_with($jail, 'nginx') => 'Web', $jail === 'recidive' => 'Recidive', str_contains($jail, 'blacklist') => 'Blacklist', default => $jail, }; } /** Liste aller Jails */ private function jailList(): array { $out = $this->f2b('status'); if (preg_match('/Jail list:\s*(.+)$/mi', $out, $m)) { $jails = array_map('trim', preg_split('/\s*,\s*/', trim($m[1]))); return array_values(array_filter($jails, fn($v) => $v !== '')); } return []; } /** fail2ban-client über sudo aufrufen */ private function f2b(string $args): string { return (string) @shell_exec('sudo -n /usr/bin/fail2ban-client '.$args.' 2>&1'); } /** Pfad zur Fail2Ban-SQLite-DB holen */ private function getDbFile(): string { $out = $this->f2b('get dbfile'); $lines = array_values(array_filter(array_map('trim', preg_split('/\r?\n/', $out)))); $path = end($lines) ?: ''; $path = preg_replace('/^`?-?\s*/', '', $path); return $path ?: '/var/lib/fail2ban/fail2ban.sqlite3'; } }