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; } /** Darstellung ableiten (Farbcode + Texte) */ private function present(?int $remaining, ?int $banAt, ?int $until): array { // Farben an dein Schema angelehnt $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) { $time = 'permanent'; $meta = $banAt ? ('seit '.$this->fmtTs($banAt)) : '—'; return [$time, $meta, $rose]; } if (is_int($remaining)) { if ($remaining > 0) { $time = $this->fmtSecs($remaining); $parts = []; if ($banAt) $parts[] = 'seit '.$this->fmtTs($banAt); if ($until) $parts[] = 'bis '.$this->fmtTs($until); $meta = $parts ? implode(' • ', $parts) : '—'; return [$time, $meta, $amber]; } // 0 Sekunden -> theoretisch abgelaufen $time = 'abgelaufen'; $meta = $banAt ? ('seit '.$this->fmtTs($banAt)) : '—'; return [$time, $meta, $muted]; } // keine Info return ['—', '—', $muted]; } /** 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 lastBanTimestamp(string $jail, string $ip): ?int { $file = '/var/log/fail2ban.log'; if (!is_readable($file)) return null; // nur das Dateiende lesen (performant, auch bei großen Logs) $tailBytes = 400000; // 400 KB $size = @filesize($file) ?: 0; $seek = max(0, $size - $tailBytes); $fh = @fopen($file, 'rb'); if (!$fh) return null; if ($seek > 0) fseek($fh, $seek); $data = stream_get_contents($fh) ?: ''; fclose($fh); // Beispiel: 2025-10-30 22:34:20,797 ... [sshd] Ban 193.46.255.244 $j = preg_quote($jail, '/'); $p = preg_quote($ip, '/'); $pattern = '/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}),\d+.*\['.$j.'\]\s+Ban\s+'.$p.'\s*$/m'; if (preg_match_all($pattern, $data, $m) && !empty($m[1])) { $date = end($m[1]); $time = end($m[2]); $dt = \DateTime::createFromFormat('Y-m-d H:i:s', "$date $time", new \DateTimeZone(date_default_timezone_get())); return $dt ? $dt->getTimestamp() : null; } return null; } private function firstMatch(string $pattern, string $haystack): ?string { return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null; } private function buildIpDetails(string $jail, array $ips, int $bantime): array { $out = []; $now = time(); foreach ($ips as $ip) { $banAt = $this->lastBanTimestamp($jail, $ip); $remaining = null; $until = null; if ($bantime === -1) { $remaining = -1; // permanent } elseif ($banAt !== null) { $remaining = max(0, $bantime - ($now - $banAt)); $until = $remaining > 0 ? ($banAt + $bantime) : null; } $out[] = [ 'ip' => $ip, 'remaining' => $remaining, // -1=permanent, null=unbekannt, 0..N Sekunden 'until' => $until, ]; } return $out; } 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); } }