279 lines
8.0 KiB
PHP
279 lines
8.0 KiB
PHP
<?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();
|
||
// }
|
||
//}
|