201 lines
6.2 KiB
PHP
201 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Ui\Security;
|
|
|
|
use Livewire\Attributes\On;
|
|
use Livewire\Component;
|
|
|
|
class Fail2banBanlist extends Component
|
|
{
|
|
/**
|
|
* null oder '*' => alle Jails
|
|
* 'recidive' => nur dieses Jail
|
|
* 'mailwolt-blacklist' etc.
|
|
*/
|
|
public ?string $jail = null;
|
|
|
|
/**
|
|
* Struktur für Blade (reine Ausgabe, keine Logik im Blade):
|
|
* [
|
|
* [
|
|
* 'ip' => '1.2.3.4',
|
|
* 'jail' => 'recidive',
|
|
* 'permanent' => false,
|
|
* 'label' => 'Temporär', // oder 'Permanent'
|
|
* 'box' => 'border-amber-400/20 bg-white/3', // Kartenstil
|
|
* 'badge' => 'border-amber-400/30 bg-amber-500/10 text-amber-200',
|
|
* 'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50',
|
|
* ],
|
|
* ...
|
|
* ]
|
|
*
|
|
* @var array<int,array{
|
|
* ip:string,jail:string,permanent:bool,label:string,box:string,badge:string,btn:string
|
|
* }>
|
|
*/
|
|
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;
|
|
}
|
|
|
|
$permanent = $this->isPermanent($j, $ip);
|
|
|
|
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,
|
|
'permanent' => $permanent,
|
|
'style' => $style,
|
|
'label' => $label,
|
|
'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 ================= */
|
|
|
|
/** Prüft via SQLite, ob der **letzte** Ban für (jail, ip) permanent ist (bantime < 0). */
|
|
private function isPermanent(string $jail, string $ip): bool
|
|
{
|
|
$db = $this->getDbFile();
|
|
if ($db === '' || !is_readable($db)) {
|
|
// Fallback: Blacklist-Jail ist per Design permanent
|
|
return $jail === 'mailwolt-blacklist';
|
|
}
|
|
|
|
$q = <<<SQL
|
|
WITH last AS (
|
|
SELECT MAX(timeofban) AS t
|
|
FROM bans
|
|
WHERE jail = '$jail' AND ip = '$ip'
|
|
)
|
|
SELECT bantime
|
|
FROM bans, last
|
|
WHERE jail = '$jail' AND ip = '$ip' AND timeofban = last.t
|
|
LIMIT 1;
|
|
SQL;
|
|
|
|
$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 ($jail === 'mailwolt-blacklist'); // Fallback
|
|
return ((int)$out) < 0;
|
|
}
|
|
|
|
/** 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';
|
|
}
|
|
}
|