CluPilotCloud/app/Services/Security/HostFirewall.php

78 lines
2.7 KiB
PHP

<?php
namespace App\Services\Security;
use App\Models\Host;
use App\Services\Secrets\SecretVault;
use App\Services\Ssh\RemoteShell;
use Throwable;
/**
* Trägt eine Adresse in die Sperrmenge eines Hosts ein oder nimmt sie wieder
* heraus — die beiden nftables-Mengen `clupilot_blocked` (IPv4) und
* `clupilot_blocked6` (IPv6), die SecureHostFirewall im Regelwerk anlegt.
* `flags timeout` an beiden Mengen (siehe dort) heißt: die Ablaufzeit geben
* wir hier mit, und der Kernel selbst nimmt die Adresse wieder heraus — kein
* eigener Aufräum-Lauf nötig für den Normalfall.
*
* Wer eine Sperre AUSLÖST, gehört nicht hierher (eigene, spätere Aufgabe) —
* das hier ist nur der Griff: eintragen, herausnehmen.
*
* Baut die Verbindung direkt über die WireGuard-Adresse des Hosts auf, nicht
* über HostStep::keyLogin()s Rückfallpfad auf die öffentliche Adresse: diese
* Klasse läuft ausschließlich gegen bereits aktive Hosts, deren Tunnel längst
* bewiesen ist, nie während der Inbetriebnahme selbst.
*
* block() und release() werfen absichtlich NICHT, wenn der Host nicht
* erreichbar ist — sie geben `false` zurück. Eine spätere Aufgabe legt den
* Sperrdatensatz in der Datenbank trotzdem an und trägt ihn beim nächsten Lauf
* erneut ein (Wiedereintragen). Eine Sperre, die nur in der Datenbank steht,
* ist sichtbar und wird nachgeholt; eine Ausnahme hier würde stattdessen den
* ganzen Zeitplan-Auftrag mitreißen, der diese Methode aufruft.
*/
class HostFirewall
{
public function __construct(private RemoteShell $shell) {}
public function block(Host $host, string $ip, int $seconds): bool
{
return $this->apply($host, sprintf(
'nft add element inet clupilot_filter %s { %s timeout %ds }',
$this->setFor($ip),
$ip,
$seconds,
));
}
public function release(Host $host, string $ip): bool
{
return $this->apply($host, sprintf(
'nft delete element inet clupilot_filter %s { %s }',
$this->setFor($ip),
$ip,
));
}
/** 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';
}
private function apply(Host $host, string $command): bool
{
try {
$this->shell->connectWithKey(
$host->wg_ip,
'root',
(string) app(SecretVault::class)->get('ssh.private_key'),
$host->ssh_host_key, // gepinnt bei EstablishSshTrust
);
return $this->shell->run($command)->ok();
} catch (Throwable) {
return false;
}
}
}