102 lines
3.7 KiB
C#
102 lines
3.7 KiB
C#
using HanaToolbox.Config;
|
|
using HanaToolbox.Logging;
|
|
using HanaToolbox.Services.Interfaces;
|
|
|
|
namespace HanaToolbox.Services;
|
|
|
|
/// <summary>
|
|
/// Applies firewall rules via firewall-cmd (firewalld).
|
|
/// Used by CronOrchestrator (non-interactive apply) and FirewallTui (interactive).
|
|
/// </summary>
|
|
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<ProcessResult> Cmd(string args, CancellationToken ct) =>
|
|
RunCmd(args, ct);
|
|
|
|
private async Task<ProcessResult> 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;
|
|
}
|
|
}
|