copy()->addDays($this->minIntervalDays) : null; if (!$this->option('force') && $nextDue && $now->lt($nextDue)) { $this->info("Übersprungen: nächste Prüfung erst ab {$nextDue->toIso8601String()} (force mit --force)."); return self::SUCCESS; } // IPs ermitteln (Installer-ENV bevorzugt) [$ipv4, $ipv6] = $this->resolvePublicIpsFromInstallerEnv(); $ipv4 = $ipv4 ?: trim((string) env('SERVER_PUBLIC_IPV4', '')) ?: null; $ipv6 = $ipv6 ?: trim((string) env('SERVER_PUBLIC_IPV6', '')) ?: null; // Kandidat für RBL (nur IPv4) $ip = $this->validIPv4($ipv4) ? $ipv4 : null; if (!$ip) { $file = trim((string) @file_get_contents('/etc/mailwolt/public_ip')); if ($this->validIPv4($file)) $ip = $file; } if (!$ip) { $curl = trim((string) @shell_exec('curl -fsS --max-time 2 ifconfig.me 2>/dev/null')); if ($this->validIPv4($curl)) $ip = $curl; } if (!$ip) $ip = '0.0.0.0'; // Abfragen (DNS) [$lists, $meta] = $this->queryRblLists($ip); $payload = [ 'ip' => $ip, 'ipv4' => $ipv4, 'ipv6' => $ipv6, 'hits' => count($lists), 'lists' => array_values($lists), // nur die tatsächlich gelisteten Zonen 'meta' => $meta, // {zone:{status, txt?}} 'checked_at' => $now->toIso8601String(), 'valid_until' => $now->copy()->addDays($this->ttlDays)->toIso8601String(), 'min_next' => $now->copy()->addDays($this->minIntervalDays)->toIso8601String(), ]; // Persistieren (DB) + in Redis spiegeln Setting::set('health.rbl', $payload); Cache::put('health.rbl', $payload, now()->addDays($this->ttlDays)); $this->info(sprintf( 'RBL: ip=%s hits=%d lists=[%s]', $payload['ip'], $payload['hits'], implode(',', $payload['lists']) )); return self::SUCCESS; } /* ---------- Helpers ---------- */ private function resolvePublicIpsFromInstallerEnv(): array { $file = '/etc/mailwolt/installer.env'; if (!is_readable($file)) return [null, null]; $ipv4 = $ipv6 = null; foreach (@file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { if ($line === '' || $line[0] === '#') continue; if (!str_contains($line, '=')) continue; [$k, $v] = array_map('trim', explode('=', $line, 2)); $v = trim($v, " \t\n\r\0\x0B\"'"); if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) $ipv4 = $v; if ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) $ipv6 = $v; } return [ $ipv4, $ipv6 ]; } private function validIPv4(?string $ip): bool { return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); } private function validIPv6(?string $ip): bool { return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); } /** * Gibt [listedZones, meta] zurück. * meta[zone] = ['status'=>'listed|clean|blocked|nx', 'txt'=>?string] */ private function queryRblLists(string $ip): array { if (!$this->validIPv4($ip)) return [[], []]; $rev = implode('.', array_reverse(explode('.', $ip))); // Kuratierte, erreichbare Zonen (ohne kaputte Subdomains) $zones = [ // Spamhaus ZEN – rate-limitiert, liefert „blocked“ bei Open Resolver 'zen.spamhaus.org', // PSBL 'psbl.surriel.com', // UCEPROTECT Level 1 'dnsbl-1.uceprotect.net', // s5h 'bl.s5h.net', ]; $listed = []; $meta = []; foreach ($zones as $zone) { $q = "{$rev}.{$zone}."; $txt = @dns_get_record($q, DNS_TXT) ?: []; $a = @dns_get_record($q, DNS_A) ?: []; // Spamhaus „blocked“ Heuristik $blocked = false; if ($zone === 'zen.spamhaus.org') { foreach ($a as $rec) { if (!empty($rec['ip']) && in_array($rec['ip'], ['127.255.255.254','127.255.255.255'], true)) { $blocked = true; break; } } if (!$blocked) { foreach ($txt as $rec) { $t = implode('', $rec['txt'] ?? []); if (stripos($t, 'open resolver') !== false) { $blocked = true; break; } } } } if ($blocked) { $meta[$zone] = ['status' => 'blocked', 'txt' => 'Spamhaus blockt – nutze privaten Resolver']; continue; } $hasA = !empty($a); $hasTXT = !empty($txt); if ($hasA || $hasTXT) { $listed[] = $zone; $meta[$zone] = ['status' => 'listed', 'txt' => $hasTXT ? ($txt[0]['txt'][0] ?? null) : null]; } else { // NXDOMAIN / sauber $meta[$zone] = ['status' => 'clean', 'txt' => null]; } } return [$listed, $meta]; } }