mailwolt/app/Models/Fail2banIpList.php

279 lines
8.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Fail2banIpList extends Model
{
protected $table = 'fail2ban_ip_lists';
protected $fillable = [
'ip',
'type',
'is_system',
];
protected $casts = [
'ip' => '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();
// }
//}