type = $type; $this->mode = $mode; $this->ip = $ip ?? ''; $this->prefill = $ip; } public function render() { return view('livewire.ui.security.modal.fail2ban-ip-modal'); } /* ---------------- actions ---------------- */ public function save(): void { $this->assertAddMode(); $ip = trim($this->ip); if (!Fail2banIpList::isValidIpOrCidr($ip)) { throw ValidationException::withMessages(['ip' => 'Ungültige IP oder CIDR.']); } // Schutz: System-/Loopback-IPs darf der User nicht manuell pflegen if (Fail2banIpList::isLoopback($ip)) { throw ValidationException::withMessages(['ip' => 'Loopback/localhost ist bereits systemseitig erlaubt und kann nicht geändert werden.']); } // Duplikate abfangen (es gibt einen Unique-Index ip+type; trotzdem user-freundlich) $exists = Fail2banIpList::where('ip', $ip)->where('type', $this->type)->exists(); if ($exists) { throw ValidationException::withMessages(['ip' => ucfirst($this->type) . ' enthält diese IP bereits.']); } // DB schreiben Fail2banIpList::create(['ip' => $ip, 'type' => $this->type]); if ($this->type === 'whitelist') { $this->writeWhitelistConfig(); // schreibt /etc/fail2ban/jail.d/mailwolt-whitelist.local $this->reloadFail2ban(); // f2b neu laden } else { // Blacklist = sofort bannen im dedizierten Jail $this->banIp($ip); } $this->dispatch('f2b:refresh'); $this->dispatch('notify', message: ucfirst($this->type) . ' aktualisiert.'); $this->closeModal(); $this->dispatch('f2b:refresh'); } public function remove(): void { $this->assertRemoveMode(); $ip = trim($this->prefill ?? $this->ip); if ($ip === '') return; // System-Whitelist darf nicht entfernt werden $row = Fail2banIpList::where('type', $this->type)->where('ip', $ip)->first(); if ($row && $row->is_system) { throw ValidationException::withMessages(['ip' => 'Systemeintrag kann nicht entfernt werden.']); } Fail2banIpList::where('type', $this->type)->where('ip', $ip)->delete(); if ($this->type === 'whitelist') { $this->writeWhitelistConfig(); $this->reloadFail2ban(); } else { $this->unbanIp($ip); } $this->dispatch('f2b:refresh'); $this->dispatch('notify', message: ucfirst($this->type) . ' Eintrag entfernt.'); $this->closeModal(); $this->dispatch('f2b:refresh'); } /* ---------------- helper ---------------- */ private function assertAddMode(): void { if ($this->mode !== 'add') throw new \LogicException('Wrong mode'); } private function assertRemoveMode(): void { if ($this->mode !== 'remove') throw new \LogicException('Wrong mode'); } private function writeWhitelistConfig(): void { // WICHTIG: inkl. System-IPs $ips = Fail2banIpList::allWhitelistForConfig(); $ignore = implode(' ', array_unique(array_filter($ips))); $content = "[DEFAULT]\nignoreip = {$ignore}\n"; // sicher in Root-Pfad schreiben (sudo tee) $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content); } private function writeRootFileViaTee(string $target, string $content): void { if (!preg_match('#^/etc/fail2ban/jail\.d/[A-Za-z0-9._-]+\.local$#', $target)) { throw new \RuntimeException("Illegal path: $target"); } $cmd = sprintf('sudo -n /usr/bin/tee %s >/dev/null', escapeshellarg($target)); $desc = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $proc = proc_open($cmd, $desc, $pipes); if (!is_resource($proc)) { throw new \RuntimeException('tee start fehlgeschlagen'); } fwrite($pipes[0], $content); fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); $code = proc_close($proc); if ($code !== 0) { throw new \RuntimeException("tee failed (code $code): $stderr $stdout"); } } private function reloadFail2ban(): void { @shell_exec('sudo -n /usr/bin/fail2ban-client reload 2>&1'); } private function banIp(string $ip): void { $ipEsc = escapeshellarg($ip); @shell_exec("sudo -n /usr/bin/fail2ban-client set mailwolt-blacklist banip {$ipEsc} 2>&1"); } private function unbanIp(string $ip): void { $ipEsc = escapeshellarg($ip); @shell_exec("sudo -n /usr/bin/fail2ban-client set mailwolt-blacklist unbanip {$ipEsc} 2>&1"); } } // //namespace App\Livewire\Ui\Security\Modal; // //use LivewireUI\Modal\ModalComponent; //use App\Models\Fail2banIpList; //use Illuminate\Validation\ValidationException; // //class Fail2banIpModal extends ModalComponent //{ // /** 'whitelist' | 'blacklist' */ // public string $type = 'whitelist'; // // /** 'add' | 'remove' */ // public string $mode = 'add'; // // /** IP/CIDR im Formular */ // public string $ip = ''; // // /** Für "remove" vorbefüllt */ // public ?string $prefill = null; // // public static function modalMaxWidth(): string { return 'lg'; } // // public function mount(string $type = 'whitelist', string $mode = 'add', ?string $ip = null): void // { // $type = strtolower($type); // $mode = strtolower($mode); // // if (!in_array($type, ['whitelist', 'blacklist'], true)) { // throw new \InvalidArgumentException('Invalid type'); // } // if (!in_array($mode, ['add', 'remove'], true)) { // throw new \InvalidArgumentException('Invalid mode'); // } // // $this->type = $type; // $this->mode = $mode; // $this->ip = $ip ?? ''; // $this->prefill = $ip; // } // // public function render() // { // return view('livewire.ui.security.modal.fail2ban-ip-modal'); // } // // /* ---------------- actions ---------------- */ // // public function save(): void // { // $this->assertAddMode(); // $ip = trim($this->ip); // // if (!$this->isValidIpOrCidr($ip)) { // throw ValidationException::withMessages(['ip' => 'Ungültige IP oder CIDR.']); // } // // // DB schreiben // Fail2banIpList::firstOrCreate(['ip' => $ip, 'type' => $this->type]); // // if ($this->type === 'whitelist') { // $this->writeWhitelistConfig(); // $this->reloadFail2ban(); // } else { // // Blacklist = sofort bannen im dedizierten Jail // $this->banIp($ip); // } // // $this->dispatch('f2b:refresh'); // $this->dispatch('notify', message: ucfirst($this->type).' aktualisiert.'); // $this->closeModal(); // $this->dispatch('f2b:refresh'); // falls du eine Liste neu laden willst // } // // public function remove(): void // { // $this->assertRemoveMode(); // $ip = trim($this->prefill ?? $this->ip); // // if ($ip === '') return; // // Fail2banIpList::where('type', $this->type)->where('ip', $ip)->delete(); // // if ($this->type === 'whitelist') { // $this->writeWhitelistConfig(); // $this->reloadFail2ban(); // } else { // // aus Blacklist-Jail entbannen, falls noch aktiv // $this->unbanIp($ip); // } // // $this->dispatch('f2b:refresh'); // $this->dispatch('notify', message: ucfirst($this->type).' Eintrag entfernt.'); // $this->closeModal(); // $this->dispatch('f2b:refresh'); // } // // /* ---------------- helper ---------------- */ // // private function assertAddMode(): void // { // if ($this->mode !== 'add') throw new \LogicException('Wrong mode'); // } // // private function assertRemoveMode(): void // { // if ($this->mode !== 'remove') throw new \LogicException('Wrong mode'); // } // // private function isValidIpOrCidr(string $s): bool // { // // IP // if (filter_var($s, FILTER_VALIDATE_IP)) return true; // // // CIDR // if (strpos($s, '/') !== false) { // [$ip, $mask] = explode('/', $s, 2); // if (!filter_var($ip, FILTER_VALIDATE_IP)) return false; // if (strpos($ip, ':') !== false) { // // IPv6 // return ctype_digit($mask) && (int)$mask >= 8 && (int)$mask <= 128; // } // // IPv4 // return ctype_digit($mask) && (int)$mask >= 8 && (int)$mask <= 32; // } // return false; // } // // private function writeWhitelistConfig(): void // { // $ips = Fail2banIpList::where('type', 'whitelist')->pluck('ip')->toArray(); // $ignore = implode(' ', array_unique(array_filter($ips))); // $content = "[DEFAULT]\nignoreip = {$ignore}\n"; // // $file = '/etc/fail2ban/jail.d/mailwolt-whitelist.local'; // $tmp = $file.'.tmp'; // @file_put_contents($tmp, $content, LOCK_EX); // @chmod($tmp, 0644); // @rename($tmp, $file); // } // // private function reloadFail2ban(): void // { // @shell_exec('sudo fail2ban-client reload 2>&1'); // } // // private function banIp(string $ip): void // { // $ipEsc = escapeshellarg($ip); // @shell_exec("sudo fail2ban-client set mailwolt-blacklist banip {$ipEsc} 2>&1"); // // optional: in DB zusätzlich behalten, damit UI konsistent ist (bereits oben getan) // } // // private function unbanIp(string $ip): void // { // $ipEsc = escapeshellarg($ip); // @shell_exec("sudo fail2ban-client set mailwolt-blacklist unbanip {$ipEsc} 2>&1"); // } //}