Files
libnftables-dotnet/src/LibNftables/NftablesClient.cs

209 lines
6.8 KiB
C#

using System.Threading;
using System.Threading.Tasks;
namespace LibNftables;
/// <summary>
/// Default high-level nftables client implementation.
/// </summary>
/// <remarks>
/// This client wraps <see cref="NftContext"/> and applies a command-centric workflow:
/// validate (dry-run), apply, snapshot, and restore.
/// </remarks>
public sealed class NftablesClient : INftablesClient
{
private readonly NftablesClientOptions _options;
private readonly System.Func<NftContext> _contextFactory;
/// <summary>
/// Initializes a new instance of the <see cref="NftablesClient"/> class.
/// </summary>
/// <param name="options">Optional client options.</param>
/// <exception cref="NftUnsupportedException">Thrown when runtime/platform is unsupported.</exception>
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
public NftablesClient(NftablesClientOptions? options = null)
: this(options, static () => new NftContext())
{
}
internal NftablesClient(NftablesClientOptions? options, System.Func<NftContext> contextFactory)
{
_options = options ?? new NftablesClientOptions();
_contextFactory = contextFactory;
NftRuntimeGuard.EnsureInitializedForLinuxX64();
}
/// <inheritdoc />
public NftValidationResult Validate(NftApplyRequest request, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
ValidateRequest(request);
try
{
CommandExecutionResult result = Execute(request, forceDryRun: true, ct);
return new NftValidationResult(true, result.Output, result.Error);
}
catch (NftValidationException ex)
{
return new NftValidationResult(false, null, ex.NativeErrorOutput ?? ex.Message);
}
}
/// <inheritdoc />
public Task<NftValidationResult> ValidateAsync(NftApplyRequest request, CancellationToken ct = default)
=> Task.FromResult(Validate(request, ct));
/// <inheritdoc />
public void Apply(NftApplyRequest request, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
ValidateRequest(request);
_ = Execute(request, forceDryRun: null, ct);
}
/// <inheritdoc />
public Task ApplyAsync(NftApplyRequest request, CancellationToken ct = default)
{
Apply(request, ct);
return Task.CompletedTask;
}
/// <inheritdoc />
public NftSnapshot Snapshot(CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
NftApplyRequest request = NftApplyRequest.FromText(_options.SnapshotCommandText, dryRun: false);
CommandExecutionResult result = Execute(request, forceDryRun: null, ct);
string snapshotText = result.Output ?? string.Empty;
string errorText = result.Error ?? string.Empty;
if (string.IsNullOrWhiteSpace(snapshotText))
{
if (ContainsAny(errorText, "Operation not permitted", "Permission denied", "CAP_NET_ADMIN"))
{
throw new NftPermissionException("Snapshot requires elevated privileges.", 0, result.Error);
}
if (!string.IsNullOrWhiteSpace(errorText))
{
throw new NftException("Snapshot returned an empty ruleset output.", nativeErrorOutput: result.Error);
}
snapshotText = "flush ruleset";
}
return new NftSnapshot(snapshotText, System.DateTimeOffset.UtcNow);
}
/// <inheritdoc />
public Task<NftSnapshot> SnapshotAsync(CancellationToken ct = default)
=> Task.FromResult(Snapshot(ct));
/// <inheritdoc />
public void Restore(NftSnapshot snapshot, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
if (snapshot is null)
{
throw new NftValidationException("Snapshot must not be null.", 0, null);
}
if (string.IsNullOrWhiteSpace(snapshot.RulesetText))
{
throw new NftValidationException("Snapshot ruleset text must not be empty.", 0, null);
}
NftApplyRequest request = NftApplyRequest.FromText(snapshot.RulesetText, dryRun: false);
_ = Execute(request, forceDryRun: null, ct);
}
/// <inheritdoc />
public Task RestoreAsync(NftSnapshot snapshot, CancellationToken ct = default)
{
Restore(snapshot, ct);
return Task.CompletedTask;
}
private CommandExecutionResult Execute(NftApplyRequest request, bool? forceDryRun, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
using NftContext ctx = _contextFactory();
ConfigureContext(ctx, request, forceDryRun);
if (!string.IsNullOrWhiteSpace(request.RulesetText))
{
ctx.RunCommand(request.RulesetText);
}
else
{
ctx.RunCommandFromFile(request.RulesetFilePath!);
}
return new CommandExecutionResult(ctx.GetOutputBuffer(), ctx.GetErrorBuffer());
}
private void ConfigureContext(NftContext ctx, NftApplyRequest request, bool? forceDryRun)
{
ctx.DryRun = forceDryRun ?? request.DryRun;
ctx.InputFlags = _options.DefaultInputFlags | request.InputFlags;
ctx.OutputFlags = _options.DefaultOutputFlags | request.OutputFlags;
ctx.DebugFlags = _options.DefaultDebugFlags | request.DebugFlags;
ctx.OptimizeFlags = _options.DefaultOptimizeFlags | request.OptimizeFlags;
if (_options.BufferOutput)
{
ctx.BufferOutput();
}
if (_options.BufferError)
{
ctx.BufferError();
}
}
private static void ValidateRequest(NftApplyRequest request)
{
if (request is null)
{
throw new NftValidationException("Request must not be null.", 0, null);
}
bool hasText = !string.IsNullOrWhiteSpace(request.RulesetText);
bool hasFile = !string.IsNullOrWhiteSpace(request.RulesetFilePath);
if (hasText == hasFile)
{
throw new NftValidationException("Exactly one source must be provided: RulesetText or RulesetFilePath.", 0, null);
}
if (hasFile && !System.IO.File.Exists(request.RulesetFilePath))
{
throw new NftValidationException($"Ruleset file was not found: '{request.RulesetFilePath}'.", 0, null);
}
}
private static bool ContainsAny(string value, params string[] candidates)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
foreach (string candidate in candidates)
{
if (value.Contains(candidate, System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private sealed record CommandExecutionResult(string? Output, string? Error);
}