first commit
This commit is contained in:
101
Services/FirewallService.cs
Normal file
101
Services/FirewallService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user