} */ public function status(Server $server): array { $res = $this->fleet->runPrivileged($server, 'ufw status verbose 2>&1'); $raw = $res['output']; $active = (bool) preg_match('/Status:\s*active/i', $raw); $rules = []; foreach (preg_split('/\R/', $raw) as $line) { $line = trim($line); // rule lines carry an action token (ALLOW/DENY/REJECT/LIMIT) if ($line !== '' && preg_match('/\b(ALLOW|DENY|REJECT|LIMIT)\b/i', $line)) { $rules[] = $line; } } return ['active' => $active, 'raw' => $raw, 'rules' => $rules]; } /** * The exact command(s) enable() will run, with the detected sshd port * already substituted — shown to the operator before applying. */ public function enablePreview(Server $server): string { return $this->enableScript($this->sshPort($server)); } /** * GUARD: allow the real sshd Port + 80/443 first, THEN enable UFW. Never * enable a firewall that would drop the current SSH session. * * @return array{ok: bool, output: string, preview: string} */ public function enable(Server $server): array { $port = $this->sshPort($server); $preview = $this->enableScript($port); // long timeout: may apt-install ufw first. $res = $this->fleet->runPrivileged($server, $this->enableScript($port), 600); return ['ok' => $res['ok'], 'output' => $res['output'], 'preview' => $preview]; } /** Turn the firewall off (ufw disable). */ public function disable(Server $server): array { $res = $this->fleet->runPrivileged($server, 'ufw disable'); return ['ok' => $res['ok'], 'output' => $res['output'], 'preview' => 'ufw disable']; } /** * Allow a TCP port through the firewall. * * @return array{ok: bool, output: string, preview: string} */ public function allow(Server $server, int $port): array { $port = $this->clampPort($port); $preview = "ufw allow {$port}/tcp"; $res = $this->fleet->runPrivileged($server, $preview); return ['ok' => $res['ok'], 'output' => $res['output'], 'preview' => $preview]; } /** * Deny a TCP port through the firewall. * * @return array{ok: bool, output: string, preview: string} */ public function deny(Server $server, int $port): array { $port = $this->clampPort($port); $preview = "ufw deny {$port}/tcp"; $res = $this->fleet->runPrivileged($server, $preview); return ['ok' => $res['ok'], 'output' => $res['output'], 'preview' => $preview]; } /** Build the guarded enable script: install ufw if missing, open ssh+80+443, then enable. */ private function enableScript(int $port): string { return '(command -v ufw >/dev/null 2>&1 || DEBIAN_FRONTEND=noninteractive apt-get install -y ufw) && ' ."ufw allow {$port}/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw --force enable"; } /** * Detect the listening sshd Port from sshd_config (incl. drop-ins). Falls * back to 22 when nothing is set explicitly. */ private function sshPort(Server $server): int { $res = $this->fleet->runPrivileged( $server, 'grep -rhiE "^[[:space:]]*Port[[:space:]]+[0-9]+" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null ' .'| awk \'{print $2}\' | head -1' ); $port = (int) trim($res['output']); return ($res['ok'] && $port >= 1 && $port <= 65535) ? $port : 22; } /** Keep a port in the valid TCP range. */ private function clampPort(int $port): int { return max(1, min(65535, $port)); } }