mailwolt/app/Console/Commands/ProbeRbl.php

169 lines
6.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\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use App\Models\Setting;
class ProbeRbl extends Command
{
protected $signature = 'rbl:probe {--force : Ignoriert Intervalle und prüft sofort}';
protected $description = 'Prüft öffentliche RBLs und speichert das Ergebnis in settings:health.rbl';
// Intervalle
private int $minIntervalDays = 7; // frühestens alle 7 Tage neu prüfen
private int $ttlDays = 14; // Ergebnis 14 Tage gültig
public function handle(): int
{
$now = now();
$existing = (array) Setting::get('health.rbl', []) ?: [];
$lastAt = isset($existing['checked_at']) ? \Illuminate\Support\Carbon::parse($existing['checked_at']) : null;
$nextDue = $lastAt ? $lastAt->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];
}
}