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;
}
}