clusev/app/Support/Os/FirewallTool.php

88 lines
3.2 KiB
PHP

<?php
namespace App\Support\Os;
/**
* Shell command strings for the host firewall (ufw or firewalld), built per OS.
*
* Safety model is preserved across both tools: enableScript() ALWAYS opens the
* real sshd port + 80/443 BEFORE turning the firewall on, so Clusev can never sever
* its own SSH session. Disable is the clean stop — never `firewall-cmd --panic-on`
* (which would drop ALL traffic, the exact lockout the guard avoids).
*/
final class FirewallTool
{
/** @param 'ufw'|'firewalld' $tool */
public function __construct(
private string $tool,
private PackageManager $pm,
private bool $canInstall,
) {}
public static function for(OsProfile $os): self
{
return new self(
$os->firewallTool === 'firewalld' ? 'firewalld' : 'ufw',
$os->managesPackages() ? PackageManager::for($os) : new PackageManager('none'),
$os->managesPackages(),
);
}
public function isFirewalld(): bool
{
return $this->tool === 'firewalld';
}
private function packageName(): string
{
return $this->tool === 'firewalld' ? 'firewalld' : 'ufw';
}
/** Guarded enable: install if needed + missing, open ssh/80/443, then turn on. */
public function enableScript(int $sshPort): string
{
$ensure = $this->canInstall ? $this->pm->ensureInstalledScript($this->packageName()).' && ' : '';
if ($this->isFirewalld()) {
$ports = "--add-port={$sshPort}/tcp --add-port=80/tcp --add-port=443/tcp";
// GUARD: open ssh/80/443 in the PERMANENT config BEFORE firewalld starts
// filtering, so starting it can never drop the operator's SSH session (a
// custom SSH port is not in the default zone). Handle both states: if the
// daemon is already running, add to permanent + reload; if it is stopped,
// write the permanent rules offline first, THEN start it.
return $ensure
.'if systemctl is-active --quiet firewalld; then '
."firewall-cmd --permanent {$ports} && systemctl enable --now firewalld && firewall-cmd --reload; "
.'else '
."firewall-offline-cmd {$ports} && systemctl enable --now firewalld; "
.'fi';
}
return $ensure
."ufw allow {$sshPort}/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw --force enable";
}
/** Clean disable (never a panic/drop-all). */
public function disableScript(): string
{
return $this->isFirewalld() ? 'systemctl disable --now firewalld' : 'ufw disable';
}
/** Shell CONDITION exiting 0 when the firewall binary is present. */
public function installedTest(): string
{
return $this->isFirewalld()
? 'command -v firewall-cmd >/dev/null 2>&1'
: 'command -v ufw >/dev/null 2>&1';
}
/** Shell CONDITION exiting 0 when the firewall is currently active. */
public function activeTest(): string
{
return $this->isFirewalld()
? 'systemctl is-active --quiet firewalld'
: 'ufw status 2>/dev/null | grep -qi "Status: active"';
}
}