209 lines
6.8 KiB
C#
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);
|
|
}
|