blockMany($host, [$ip => $seconds]); } /** * Mehrere Adressen desselben Hosts in EINER SSH-Sitzung. * * `ScanForIntrusions::reapplyActiveBlocks()` trug bisher je aktiver Sperre * einzeln ein, und jede Eintragung baute ihre eigene Verbindung auf: bei * dreissig Sperren dreissig Handshakes pro Minute, seriell, auf dem * einzigen Provisionierungs-Arbeiter — also genau WÄHREND eines Angriffs * auf demselben Arbeiter, der die bezahlte Kundenbereitstellung fährt. * * `nft` nimmt mehrere Elemente in einem Aufruf: `{ a timeout 60s, b * timeout 90s }`. Je Adressfamilie einer, weil v4 und v6 in getrennten * Mengen leben — also höchstens zwei Befehle, eine Verbindung. * * Jede Adresse behält ihre EIGENE Restlaufzeit. Eine gemeinsame Dauer für * das ganze Bündel wäre bequemer und würde genau die Zusicherung * verwässern, für die es einen eigenen Test gibt. * * @param array $seconds Adresse => Restlaufzeit in Sekunden */ public function blockMany(Host $host, array $seconds): bool { /** @var array> */ $elements = []; foreach ($seconds as $ip => $duration) { $ip = (string) $ip; if (! $this->isWellFormed($host, $ip)) { return false; } $elements[$this->setFor($ip)][] = sprintf('%s timeout %ds', $ip, (int) $duration); } if ($elements === []) { return true; } $commands = []; foreach ($elements as $set => $entries) { $commands[] = sprintf( 'nft add element inet clupilot_filter %s { %s }', $set, implode(', ', $entries), ); } return $this->apply($host, $commands, implode(', ', array_keys($seconds))); } public function release(Host $host, string $ip): bool { if (! $this->isWellFormed($host, $ip)) { return false; } return $this->apply($host, [sprintf( 'nft delete element inet clupilot_filter %s { %s }', $this->setFor($ip), $ip, )], $ip); } /** * Wohlgeformt genug, um in eine Wurzel-Shell zu wandern? * * Meldet und verweigert statt still weiterzumachen: eine Adresse, die hier * ankommt und keine ist, ist entweder ein Versuch oder ein Fehler weiter * oben — beides will man sehen. */ private function isWellFormed(Host $host, string $ip): bool { if (filter_var($ip, FILTER_VALIDATE_IP) !== false) { return true; } report(new RuntimeException( "HostFirewall: abgewiesene Adresse [{$ip}] fuer Host [{$host->name}] — keine gueltige IP, nichts ausgefuehrt." )); return false; } /** Die Adressfamilie entscheidet über die Menge — v4 und v6 leben getrennt. */ private function setFor(string $ip): string { return str_contains($ip, ':') ? 'clupilot_blocked6' : 'clupilot_blocked'; } /** * @param array $commands alle in EINER Sitzung */ private function apply(Host $host, array $commands, string $subject): bool { try { $this->shell->connectWithKey( $host->wg_ip, 'root', (string) app(SecretVault::class)->get('ssh.private_key'), $host->ssh_host_key, // gepinnt bei EstablishSshTrust ); $ok = true; foreach ($commands as $command) { // Nicht abbrechen beim ersten Fehlschlag: die Verbindung steht // ohnehin schon, und v6 soll nicht deshalb ausfallen, weil v4 // gerade nicht ging. $ok = $this->shell->run($command)->ok() && $ok; } } catch (Throwable) { $ok = false; } if (! $ok) { report(new RuntimeException( "HostFirewall: [{$subject}] auf Host [{$host->name}] nicht in die Firewall geschrieben " .'— der Sperrdatensatz steht, die Regel nicht.' )); } return $ok; } }