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); }