using System.Threading;
using System.Threading.Tasks;
namespace LibNftables;
///
/// Default high-level nftables client implementation.
///
///
/// This client wraps and applies a command-centric workflow:
/// validate (dry-run), apply, snapshot, and restore.
///
public sealed class NftablesClient : INftablesClient
{
private readonly NftablesClientOptions _options;
private readonly System.Func _contextFactory;
///
/// Initializes a new instance of the class.
///
/// Optional client options.
/// Thrown when runtime/platform is unsupported.
/// Thrown when required native runtime components cannot be loaded.
public NftablesClient(NftablesClientOptions? options = null)
: this(options, static () => new NftContext())
{
}
internal NftablesClient(NftablesClientOptions? options, System.Func contextFactory)
{
_options = options ?? new NftablesClientOptions();
_contextFactory = contextFactory;
NftRuntimeGuard.EnsureInitializedForLinuxX64();
}
///
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);
}
}
///
public Task ValidateAsync(NftApplyRequest request, CancellationToken ct = default)
=> Task.FromResult(Validate(request, ct));
///
public void Apply(NftApplyRequest request, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
ValidateRequest(request);
_ = Execute(request, forceDryRun: null, ct);
}
///
public Task ApplyAsync(NftApplyRequest request, CancellationToken ct = default)
{
Apply(request, ct);
return Task.CompletedTask;
}
///
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);
}
///
public Task SnapshotAsync(CancellationToken ct = default)
=> Task.FromResult(Snapshot(ct));
///
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);
}
///
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);
}