using HanaToolbox.Config; using HanaToolbox.Logging; using HanaToolbox.Services.Interfaces; namespace HanaToolbox.Services; /// /// Applies firewall rules via firewall-cmd (firewalld). /// Used by CronOrchestrator (non-interactive apply) and FirewallTui (interactive). /// public sealed class FirewallService(IProcessRunner runner, AppLogger logger) : IFirewallService { public async Task ApplyAsync(FirewallConfig config, CancellationToken ct = default) { logger.Step("Applying firewall rules (firewall-cmd)..."); if (config.FlushBeforeApply) { logger.Step("Flushing existing rules..."); await FlushAsync(ct); } else { // Remove insecure catch-all rules as a safety measure await Cmd("--remove-port=0-65535/tcp", ct); await Cmd("--remove-service=ssh", ct); await Cmd("--remove-port=22/tcp", ct); } foreach (var svc in config.Services) { ct.ThrowIfCancellationRequested(); switch (svc.Decision) { case FirewallDecision.All: foreach (var port in svc.Ports) { logger.Step($"Opening port {port}/tcp globally ({svc.Name})"); await Cmd($"--add-port={port}/tcp", ct); } break; case FirewallDecision.Ip: foreach (var ip in svc.AllowedIps) { foreach (var port in svc.Ports) { logger.Step($"Restricting port {port}/tcp to {ip} ({svc.Name})"); await Cmd( $"--add-rich-rule=rule family='ipv4' source address='{ip}' port port='{port}' protocol='tcp' accept", ct); } } break; case FirewallDecision.Skip: logger.Info($"Skipping {svc.Name}"); break; } } // Save runtime config to permanent logger.Step("Saving rules permanently..."); await Cmd("--runtime-to-permanent", ct); logger.Success("Firewall rules applied."); } private async Task FlushAsync(CancellationToken ct) { // list and remove all services var services = (await RunCmd("--list-services", ct)).StdOut.Trim(); foreach (var s in services.Split(' ', StringSplitOptions.RemoveEmptyEntries)) await Cmd($"--remove-service={s}", ct); // list and remove all ports var ports = (await RunCmd("--list-ports", ct)).StdOut.Trim(); foreach (var p in ports.Split(' ', StringSplitOptions.RemoveEmptyEntries)) await Cmd($"--remove-port={p}", ct); // list and remove rich rules var richRules = (await RunCmd("--list-rich-rules", ct)).StdOut.Trim(); foreach (var rule in richRules.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { if (!string.IsNullOrWhiteSpace(rule)) await Cmd($"--remove-rich-rule={rule.Trim()}", ct); } } private Task Cmd(string args, CancellationToken ct) => RunCmd(args, ct); private async Task RunCmd(string args, CancellationToken ct) { // Split simple args by space (they won't contain spaces except rich-rules) // Use shell to handle complex rich-rule strings var result = await runner.RunAsync( "/bin/bash", ["-c", $"firewall-cmd {args}"], ct); return result; } }