'string', 'type' => 'string', 'is_system' => 'boolean', ]; public const TYPE_WHITELIST = 'whitelist'; public const TYPE_BLACKLIST = 'blacklist'; /* =========================== Boot-Hooks (Schutz & Normalisierung) =========================== */ protected static function booted() { // Normalisierung & Loopback-Flag setzen static::saving(function (self $m) { $m->ip = trim($m->ip); if (!self::isValidIpOrCidr($m->ip)) { throw new \InvalidArgumentException("Ungültige IP/CIDR: {$m->ip}"); } // Loopback immer als System markieren if (self::isLoopback($m->ip)) { $m->is_system = true; $m->type = self::TYPE_WHITELIST; // Loopback gehört auf die Whitelist } // Systemeinträge dürfen nicht in die Blacklist if ($m->is_system && $m->type === self::TYPE_BLACKLIST) { throw new \InvalidArgumentException("Systemeinträge dürfen nicht auf die Blacklist."); } }); // Systemeinträge sind unveränderlich (bis auf interne Seeds/Maintenance – dann per DB direkt ändern) static::updating(function (self $m) { if ($m->getOriginal('is_system')) { // Erlaube nur no-op Updates (z. B. Timestamps), aber blocke ip/type Änderungen $blocked = $m->isDirty('ip') || $m->isDirty('type') || $m->isDirty('is_system'); if ($blocked) { throw new \RuntimeException("Systemeinträge können nicht geändert werden."); } } }); static::deleting(function (self $m) { if ($m->is_system) { throw new \RuntimeException("Systemeintrag kann nicht gelöscht werden."); } }); } /* =========================== Scopes =========================== */ public function scopeWhitelist($q) { return $q->where('type', self::TYPE_WHITELIST); } public function scopeBlacklist($q) { return $q->where('type', self::TYPE_BLACKLIST); } // Für UI: blende Systemeinträge aus public function scopeVisible($q) { return $q->where('is_system', false); } // Kombiniert: z. B. Fail2banIpList::visible()->whitelist()->get(); public function scopeVisibleWhitelist($q) { return $q->visible()->whitelist(); } public function scopeVisibleBlacklist($q) { return $q->visible()->blacklist(); } /* =========================== Helper-Listen =========================== */ // Für UI-Listen (ohne System) public static function whitelistArray(): array { return static::where('type', self::TYPE_WHITELIST) ->where('is_system', false) ->pluck('ip')->all(); } public static function blacklistArray(): array { return static::where('type', self::TYPE_BLACKLIST) ->where('is_system', false) ->pluck('ip')->all(); } // Für das Schreiben der Fail2ban-Whitelist-Datei (inkl. System!) public static function allWhitelistForConfig(): array { return static::where('type', self::TYPE_WHITELIST) ->pluck('ip')->all(); } /* =========================== Validierung =========================== */ // Erlaubt IPv4/IPv6, optional mit CIDR (/0..32 bzw. /0..128) public static function isValidIpOrCidr(string $value): bool { $value = trim($value); // IP ohne CIDR if (filter_var($value, FILTER_VALIDATE_IP)) { return true; } // IP/CIDR if (strpos($value, '/') !== false) { [$ip, $prefix] = explode('/', $value, 2); if (!ctype_digit($prefix)) { return false; } $prefix = (int)$prefix; if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return $prefix >= 0 && $prefix <= 32; } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { return $prefix >= 0 && $prefix <= 128; } return false; } return false; } // Loopback-Erkennung (IPv4 127.0.0.0/8, IPv6 ::1/128) public static function isLoopback(string $value): bool { $value = trim($value); // Klartext-Fälle if (in_array($value, ['127.0.0.1', '127.0.0.1/8', '::1', '::1/128'], true)) { return true; } // IPv4 Loopback Bereich if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return str_starts_with($value, '127.'); } // IPv4-CIDR Loopback if (strpos($value, '/') !== false) { [$ip, $prefix] = explode('/', $value, 2); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && ctype_digit($prefix)) { $prefix = (int)$prefix; // Prüfe, ob Netz 127.0.0.0/8 überlappt return self::cidrOverlaps($ip, $prefix, '127.0.0.0', 8); } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && ctype_digit($prefix)) { $prefix = (int)$prefix; // Prüfe, ob ::1/128 überlappt (nur exakt ::1) return self::cidrOverlaps($ip, $prefix, '::1', 128); } } // IPv6 single if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { return $value === '::1'; } return false; } // Simple Overlap-Check für IPv4/IPv6 Netze private static function cidrOverlaps(string $ip, int $prefix, string $netIp, int $netPrefix): bool { $a = inet_pton($ip); $b = inet_pton($netIp); if ($a === false || $b === false || strlen($a) !== strlen($b)) { return false; } $len = strlen($a); $bytes = intdiv(max($prefix, $netPrefix), 8); $bits = max($prefix, $netPrefix) % 8; // Netzmaske anwenden (auf die längere Präfixlänge) for ($i = 0; $i < $bytes; $i++) { if ($a[$i] !== $b[$i]) return false; } if ($bits > 0) { $mask = chr(0xFF << (8 - $bits)); if ((ord($a[$bytes]) & ord($mask)) !== (ord($b[$bytes]) & ord($mask))) { return false; } } return true; } } // //namespace App\Models; // //use Illuminate\Database\Eloquent\Model; // //class Fail2banIpList extends Model //{ // protected $fillable = [ // 'ip', // 'type', // ]; // // protected $casts = [ // 'ip' => 'string', // 'type' => 'string', // ]; // // const TYPE_WHITELIST = 'whitelist'; // const TYPE_BLACKLIST = 'blacklist'; // // /** // * Scopes // */ // public function scopeWhitelist($query) // { // return $query->where('type', self::TYPE_WHITELIST); // } // // public function scopeBlacklist($query) // { // return $query->where('type', self::TYPE_BLACKLIST); // } // // /** // * Validiert grob die IP. // */ // public function isValidIp(): bool // { // return filter_var($this->ip, FILTER_VALIDATE_IP) !== false; // } // // /** // * Gibt Liste aller Whitelist-IPs als Array zurück. // */ // public static function whitelistArray(): array // { // return static::where('type', self::TYPE_WHITELIST)->pluck('ip')->all(); // } // // /** // * Gibt Liste aller Blacklist-IPs als Array zurück. // */ // public static function blacklistArray(): array // { // return static::where('type', self::TYPE_BLACKLIST)->pluck('ip')->all(); // } //}