jail = $jail; $this->load(); } public function refresh(): void { $this->load(true); } public function render() { return view('livewire.ui.security.modal.fail2-ban-jail-modal'); } /* ---------------- intern ---------------- */ protected function load(bool $force = false): void { $jail = $this->jail; // Aktuell gebannte IPs [, $s] = $this->f2b('status '.escapeshellarg($jail)); $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: ''; $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : []; // bantime für Jail $bantime = $this->getBantime($jail); $rows = []; foreach ($ips as $ip) { $banAt = $this->lastBanTimestamp($jail, $ip); // Unix-Timestamp oder null $remaining = null; $until = null; if ($banAt !== null) { if ($bantime === -1) { $remaining = -1; } else { $remaining = max(0, $bantime - (time() - $banAt)); $until = $remaining > 0 ? $banAt + $bantime : null; } } // Darstellung [$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until); $rows[] = [ 'ip' => $ip, 'bantime' => $bantime, 'banned_at' => $banAt, 'remaining' => $remaining, 'until' => $until, 'time_text' => $timeText, 'meta_text' => $metaText, 'box_class' => $boxClass, ]; } $this->rows = $rows; } /** letzte "Ban "-Zeile -> Unix-Timestamp (robust über zgrep/journal/jail-tail) */ private function lastBanTimestamp(string $jail, string $ip): ?int { $sudo = '/usr/bin/sudo'; $zgrep = '/usr/bin/zgrep'; $journal = '/usr/bin/journalctl'; $tail = '/usr/bin/tail'; $needle = sprintf('[%s] Ban %s', $jail, $ip); $q = escapeshellarg($needle); // 1) zgrep über alle fail2ban-Logs (inkl. Rotation .N, .gz) $cmd1 = "$sudo -n $zgrep -h $q /var/log/fail2ban.log* 2>/dev/null | $tail -n 1"; $line = trim((string)@shell_exec($cmd1)); // 2) Fallback: journald (ohne grep-Pipe, mit -g) if ($line === '') { $cmd2 = "$sudo -n $journal -u fail2ban --since '14 days ago' --no-pager -g $q 2>/dev/null | $tail -n 1"; $line = trim((string)@shell_exec($cmd2)); } if ($line === '') return null; // Zeitstempel extrahieren (YYYY-MM-DD HH:MM:SS) if (preg_match('/(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})/', $line, $m)) { $ts = strtotime($m[1].' '.$m[2]); return $ts ?: null; } return null; } /** baut Details inkl. Restzeit; wenn kein Timestamp gefunden: ~bantime als Approximation */ private function buildIpDetails(string $jail, array $ips, int $bantime): array { $now = time(); $out = []; foreach ($ips as $ip) { $banAt = $this->lastBanTimestamp($jail, $ip); $remaining = null; $until = null; if ($bantime === -1) { $remaining = -1; } elseif ($banAt !== null) { $remaining = max(0, $bantime - ($now - $banAt)); $until = $remaining > 0 ? ($banAt + $bantime) : null; } else { // kein Ban-Timestamp gefunden → Approximation anzeigen $remaining = -2; // Kennzeichen für "~bantime" } $out[] = [ 'ip' => $ip, 'banned_at' => $banAt, 'remaining' => $remaining, // -1=permanent, -2≈approx, 0..Sek, null=unbekannt 'until' => $until, ]; } return $out; } /** Darstellung (permanent / Restzeit / abgelaufen / approx / unbekannt) */ private function present(?int $remaining, ?int $banAt, ?int $until): array { $green = 'border-emerald-400/30 bg-emerald-500/10'; $amber = 'border-amber-400/30 bg-amber-500/10'; $rose = 'border-rose-400/30 bg-rose-500/10'; $muted = 'border-white/10 bg-white/5'; if ($remaining === -1) { return ['permanent', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $rose]; } if ($remaining === -2) { // Approximation: Bannzeit ohne Startzeitpunkt return ['~ '.$this->fmtSecs((int) $this->getApproxBantime()), '—', $amber]; } if (is_int($remaining)) { if ($remaining > 0) { $meta = []; if ($banAt) $meta[] = 'seit '.$this->fmtTs($banAt); if ($until) $meta[] = 'bis '.$this->fmtTs($until); return [$this->fmtSecs($remaining), $meta ? implode(' • ', $meta) : '—', $amber]; } return ['abgelaufen', $banAt ? ('seit '.$this->fmtTs($banAt)) : '—', $muted]; } return ['—', '—', $muted]; } // Bantime als Approximation (du kannst das einfach aus dem aufrufenden Kontext übergeben; // hier fallback 600) private function getApproxBantime(): int { return 600; } /** Darstellung ableiten (Farbcode + Texte) */ /** sudo + fail2ban-client */ private function f2b(string $args): array { $sudo = '/usr/bin/sudo'; $f2b = '/usr/bin/fail2ban-client'; $out = (string) @shell_exec("timeout 3 $sudo -n $f2b $args 2>&1"); $ok = stripos($out, 'Status') !== false || stripos($out, 'Jail list') !== false || stripos($out, 'pong') !== false; return [$ok, $out]; } private function getBantime(string $jail): int { [, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime'); $val = trim($out); if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0]; return 600; } /** letzte "Ban "-Zeile parsen -> Unix-Timestamp */ /** letzte "Ban "-Zeile -> Unix-Timestamp aus /var/log/fail2ban.log */ private function firstMatch(string $pattern, string $haystack): ?string { return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; } private function fmtSecs(int $s): string { // kompakt: 1h 23m 45s / 05m 10s / 12s $h = intdiv($s, 3600); $m = intdiv($s % 3600, 60); $r = $s % 60; if ($h > 0) return sprintf('%dh %02dm %02ds', $h, $m, $r); if ($m > 0) return sprintf('%02dm %02ds', $m, $r); return sprintf('%ds', $r); } private function fmtTs(int $ts): string { // 29.10. 18:07 return date('d.m. H:i', $ts); } }