diff --git a/README.md b/README.md
index 2bad134..8f9d8e9 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,15 @@
# libnftables-dotnet
-`libnftables-dotnet` is a command-centric .NET wrapper over system-installed `libnftables`, with low-level SWIG-generated bindings and a small managed API for common workflows.
+`libnftables-dotnet` is a typed-first .NET wrapper over system-installed `libnftables`, with a high-level object model for common workflows and low-level SWIG-generated bindings for advanced control.
## Current Scope
This library is intentionally narrow.
- High-level managed API:
- - `Validate`
- - `Apply`
+ - typed `NftRuleset` / `NftTable` / `NftSet` authoring
+ - `ValidateRuleset`
+ - `ApplyRuleset`
- `Snapshot`
- `Restore`
- Low-level managed wrapper:
@@ -16,7 +17,7 @@ This library is intentionally narrow.
Non-goals for the current release:
-- Typed .NET models for tables, chains, rules, sets, or maps
+- Typed rule expressions, maps, and snapshot parsing back into object models
- Event monitoring or subscriptions
- Cross-platform support beyond Linux x64
@@ -67,13 +68,31 @@ using LibNftables;
INftablesClient client = new NftablesClient();
-var validation = client.Validate(NftApplyRequest.FromText("add table inet my_table"));
+var ruleset = new NftRuleset();
+var table = new NftTable
+{
+ Family = NftFamily.Inet,
+ Name = "filter",
+};
+var blocked = new NftSet
+{
+ Name = "blocked_ipv4",
+ Type = NftSetType.Ipv4Address,
+};
+blocked.Elements.Add("10.0.0.1");
+blocked.Elements.Add("10.0.0.2");
+table.Sets.Add(blocked);
+ruleset.Tables.Add(table);
+
+var validation = client.ValidateRuleset(ruleset);
if (validation.IsValid)
{
- client.Apply(NftApplyRequest.FromText("add table inet my_table"));
+ client.ApplyRuleset(ruleset);
}
```
+Raw command text remains available through `NftApplyRequest` as a fallback for nft syntax not yet modeled by the typed API.
+
## Low-Level Example
```csharp
@@ -113,7 +132,7 @@ Some operations require elevated privileges or `CAP_NET_ADMIN`, especially when
### Validation failures
-`Validate` returns `IsValid = false` for invalid nft syntax. `Apply` and `Restore` throw when the request shape is invalid or native parsing fails.
+`ValidateRuleset` returns `IsValid = false` for invalid nft syntax after rendering the typed model. `ApplyRuleset` and `Restore` throw when the typed/request shape is invalid or native parsing fails.
## Bindings and Regeneration
diff --git a/src/LibNftables/INftablesClient.cs b/src/LibNftables/INftablesClient.cs
index 22504d0..c143859 100644
--- a/src/LibNftables/INftablesClient.cs
+++ b/src/LibNftables/INftablesClient.cs
@@ -18,6 +18,19 @@ public interface INftablesClient
/// Thrown when required native runtime components cannot be loaded.
NftValidationResult Validate(NftApplyRequest request, System.Threading.CancellationToken ct = default);
+ ///
+ /// Validates a typed nftables ruleset by rendering it to nft command text in dry-run mode.
+ ///
+ /// The typed ruleset to validate.
+ /// A cancellation token.
+ ///
+ /// A validation result. Invalid nft syntax or schema errors return = .
+ ///
+ /// Thrown when the typed model itself is invalid before native validation begins.
+ /// Thrown when runtime/platform is unsupported.
+ /// Thrown when required native runtime components cannot be loaded.
+ NftValidationResult ValidateRuleset(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
+
///
/// Asynchronously validates a ruleset request using dry-run mode.
///
@@ -29,6 +42,17 @@ public interface INftablesClient
/// Thrown when required native runtime components cannot be loaded.
System.Threading.Tasks.Task ValidateAsync(NftApplyRequest request, System.Threading.CancellationToken ct = default);
+ ///
+ /// Asynchronously validates a typed nftables ruleset by rendering it to nft command text in dry-run mode.
+ ///
+ /// The typed ruleset to validate.
+ /// A cancellation token.
+ /// A task that resolves to a validation result.
+ /// Thrown when the typed model itself is invalid before native validation begins.
+ /// Thrown when runtime/platform is unsupported.
+ /// Thrown when required native runtime components cannot be loaded.
+ System.Threading.Tasks.Task ValidateRulesetAsync(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
+
///
/// Applies a ruleset request.
///
@@ -41,6 +65,18 @@ public interface INftablesClient
/// Thrown for other native execution failures.
void Apply(NftApplyRequest request, System.Threading.CancellationToken ct = default);
+ ///
+ /// Applies a typed nftables ruleset by rendering it to nft command text.
+ ///
+ /// The typed ruleset to apply.
+ /// A cancellation token.
+ /// Thrown when the typed model itself is invalid or the rendered ruleset cannot be parsed/validated.
+ /// Thrown when insufficient privileges are available for runtime operation.
+ /// Thrown when runtime/platform is unsupported.
+ /// Thrown when required native runtime components cannot be loaded.
+ /// Thrown for other native execution failures.
+ void ApplyRuleset(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
+
///
/// Asynchronously applies a ruleset request.
///
@@ -54,6 +90,19 @@ public interface INftablesClient
/// Thrown for other native execution failures.
System.Threading.Tasks.Task ApplyAsync(NftApplyRequest request, System.Threading.CancellationToken ct = default);
+ ///
+ /// Asynchronously applies a typed nftables ruleset by rendering it to nft command text.
+ ///
+ /// The typed ruleset to apply.
+ /// A cancellation token.
+ /// A completed task when apply succeeds.
+ /// Thrown when the typed model itself is invalid or the rendered ruleset cannot be parsed/validated.
+ /// Thrown when insufficient privileges are available for runtime operation.
+ /// Thrown when runtime/platform is unsupported.
+ /// Thrown when required native runtime components cannot be loaded.
+ /// Thrown for other native execution failures.
+ System.Threading.Tasks.Task ApplyRulesetAsync(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
+
///
/// Captures the current nftables ruleset from the system.
///
diff --git a/src/LibNftables/NftFamily.cs b/src/LibNftables/NftFamily.cs
new file mode 100644
index 0000000..850e5ff
--- /dev/null
+++ b/src/LibNftables/NftFamily.cs
@@ -0,0 +1,37 @@
+namespace LibNftables;
+
+///
+/// Supported nftables address families for typed ruleset authoring.
+///
+public enum NftFamily
+{
+ ///
+ /// The inet family.
+ ///
+ Inet,
+
+ ///
+ /// The ip family.
+ ///
+ Ip,
+
+ ///
+ /// The ip6 family.
+ ///
+ Ip6,
+
+ ///
+ /// The arp family.
+ ///
+ Arp,
+
+ ///
+ /// The bridge family.
+ ///
+ Bridge,
+
+ ///
+ /// The netdev family.
+ ///
+ Netdev,
+}
diff --git a/src/LibNftables/NftRuleset.cs b/src/LibNftables/NftRuleset.cs
new file mode 100644
index 0000000..0fb933b
--- /dev/null
+++ b/src/LibNftables/NftRuleset.cs
@@ -0,0 +1,12 @@
+namespace LibNftables;
+
+///
+/// Represents a typed nftables ruleset authored through the high-level API.
+///
+public sealed class NftRuleset
+{
+ ///
+ /// Gets the tables included in this ruleset.
+ ///
+ public IList Tables { get; } = new List();
+}
diff --git a/src/LibNftables/NftRulesetRenderer.cs b/src/LibNftables/NftRulesetRenderer.cs
new file mode 100644
index 0000000..643b5a2
--- /dev/null
+++ b/src/LibNftables/NftRulesetRenderer.cs
@@ -0,0 +1,198 @@
+using System.Text;
+
+namespace LibNftables;
+
+internal static class NftRulesetRenderer
+{
+ internal static string Render(NftRuleset ruleset)
+ {
+ if (ruleset is null)
+ {
+ throw new NftValidationException("Ruleset must not be null.", 0, null);
+ }
+
+ if (ruleset.Tables.Count == 0)
+ {
+ throw new NftValidationException("Ruleset must contain at least one table.", 0, null);
+ }
+
+ var tableNames = new HashSet(StringComparer.Ordinal);
+ var builder = new StringBuilder();
+
+ foreach (NftTable table in ruleset.Tables)
+ {
+ ValidateTable(table);
+
+ string tableKey = $"{table.Family}:{table.Name}";
+ if (!tableNames.Add(tableKey))
+ {
+ throw new NftValidationException(
+ $"Ruleset contains duplicate table '{table.Name}' in family '{ToKeyword(table.Family)}'.",
+ 0,
+ null);
+ }
+
+ builder.Append("add table ")
+ .Append(ToKeyword(table.Family))
+ .Append(' ')
+ .Append(table.Name)
+ .AppendLine();
+
+ var setNames = new HashSet(StringComparer.Ordinal);
+ foreach (NftSet set in table.Sets)
+ {
+ ValidateSet(set, table);
+
+ if (!setNames.Add(set.Name!))
+ {
+ throw new NftValidationException(
+ $"Table '{table.Name}' contains duplicate set '{set.Name}'.",
+ 0,
+ null);
+ }
+
+ builder.Append("add set ")
+ .Append(ToKeyword(table.Family))
+ .Append(' ')
+ .Append(table.Name)
+ .Append(' ')
+ .Append(set.Name)
+ .Append(" { type ")
+ .Append(GetTypeExpression(set))
+ .Append(';');
+
+ if (set.Elements.Count > 0)
+ {
+ builder.Append(" elements = { ");
+
+ for (int i = 0; i < set.Elements.Count; i++)
+ {
+ if (i > 0)
+ {
+ builder.Append(", ");
+ }
+
+ builder.Append(set.Elements[i]);
+ }
+
+ builder.Append(" };");
+ }
+
+ builder.Append(" }")
+ .AppendLine();
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ private static void ValidateTable(NftTable table)
+ {
+ if (table is null)
+ {
+ throw new NftValidationException("Ruleset tables must not contain null entries.", 0, null);
+ }
+
+ ValidateIdentifier(table.Name, "Table name");
+ }
+
+ private static void ValidateSet(NftSet set, NftTable table)
+ {
+ if (set is null)
+ {
+ throw new NftValidationException(
+ $"Table '{table.Name}' contains a null set definition.",
+ 0,
+ null);
+ }
+
+ ValidateIdentifier(set.Name, "Set name");
+
+ bool hasTypedType = set.Type.HasValue;
+ bool hasCustomType = !string.IsNullOrWhiteSpace(set.CustomTypeExpression);
+
+ if (hasTypedType == hasCustomType)
+ {
+ throw new NftValidationException(
+ $"Set '{set.Name}' must specify exactly one type source: Type or CustomTypeExpression.",
+ 0,
+ null);
+ }
+
+ if (hasCustomType)
+ {
+ ValidateToken(set.CustomTypeExpression!, $"Set '{set.Name}' custom type");
+ }
+
+ for (int i = 0; i < set.Elements.Count; i++)
+ {
+ string? element = set.Elements[i];
+ if (string.IsNullOrWhiteSpace(element))
+ {
+ throw new NftValidationException(
+ $"Set '{set.Name}' contains an empty element at index {i}.",
+ 0,
+ null);
+ }
+
+ ValidateToken(element, $"Set '{set.Name}' element");
+ }
+ }
+
+ private static void ValidateIdentifier(string? value, string fieldName)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new NftValidationException($"{fieldName} must not be null, empty, or whitespace.", 0, null);
+ }
+
+ foreach (char ch in value)
+ {
+ if (char.IsWhiteSpace(ch) || ch is '{' or '}' or ';' or '"' or '\'')
+ {
+ throw new NftValidationException(
+ $"{fieldName} contains unsupported character '{ch}'.",
+ 0,
+ null);
+ }
+ }
+ }
+
+ private static void ValidateToken(string value, string fieldName)
+ {
+ foreach (char ch in value)
+ {
+ if (ch is '\r' or '\n' or ';')
+ {
+ throw new NftValidationException(
+ $"{fieldName} contains unsupported character '{ch}'.",
+ 0,
+ null);
+ }
+ }
+ }
+
+ private static string GetTypeExpression(NftSet set)
+ => set.Type switch
+ {
+ NftSetType.Ipv4Address => "ipv4_addr",
+ NftSetType.Ipv6Address => "ipv6_addr",
+ NftSetType.InetService => "inet_service",
+ NftSetType.EtherAddress => "ether_addr",
+ NftSetType.InterfaceName => "ifname",
+ NftSetType.Mark => "mark",
+ _ => set.CustomTypeExpression!,
+ };
+
+ private static string ToKeyword(NftFamily family)
+ => family switch
+ {
+ NftFamily.Inet => "inet",
+ NftFamily.Ip => "ip",
+ NftFamily.Ip6 => "ip6",
+ NftFamily.Arp => "arp",
+ NftFamily.Bridge => "bridge",
+ NftFamily.Netdev => "netdev",
+ _ => throw new NftValidationException($"Unsupported nft family '{family}'.", 0, null),
+ };
+}
diff --git a/src/LibNftables/NftSet.cs b/src/LibNftables/NftSet.cs
new file mode 100644
index 0000000..6cc1f6f
--- /dev/null
+++ b/src/LibNftables/NftSet.cs
@@ -0,0 +1,37 @@
+namespace LibNftables;
+
+///
+/// Represents a typed nftables set definition backed by native C# collections.
+///
+public sealed class NftSet
+{
+ ///
+ /// Gets or sets the set name.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets a common typed set element type.
+ ///
+ ///
+ /// Set either or .
+ ///
+ public NftSetType? Type { get; set; }
+
+ ///
+ /// Gets or sets a custom nftables type expression for the set.
+ ///
+ ///
+ /// This exists as a fallback for types not yet modeled by .
+ /// Set either or .
+ ///
+ public string? CustomTypeExpression { get; set; }
+
+ ///
+ /// Gets the set elements to declare inline with the set definition.
+ ///
+ ///
+ /// Values are rendered as nftables literals and should already be valid for the selected type.
+ ///
+ public IList Elements { get; } = new List();
+}
diff --git a/src/LibNftables/NftSetType.cs b/src/LibNftables/NftSetType.cs
new file mode 100644
index 0000000..bccb39a
--- /dev/null
+++ b/src/LibNftables/NftSetType.cs
@@ -0,0 +1,37 @@
+namespace LibNftables;
+
+///
+/// Common typed nftables set element types supported by the high-level API.
+///
+public enum NftSetType
+{
+ ///
+ /// IPv4 addresses (ipv4_addr).
+ ///
+ Ipv4Address,
+
+ ///
+ /// IPv6 addresses (ipv6_addr).
+ ///
+ Ipv6Address,
+
+ ///
+ /// Internet service/port values (inet_service).
+ ///
+ InetService,
+
+ ///
+ /// Ethernet/MAC addresses (ether_addr).
+ ///
+ EtherAddress,
+
+ ///
+ /// Interface names (ifname).
+ ///
+ InterfaceName,
+
+ ///
+ /// Packet marks (mark).
+ ///
+ Mark,
+}
diff --git a/src/LibNftables/NftTable.cs b/src/LibNftables/NftTable.cs
new file mode 100644
index 0000000..8928e93
--- /dev/null
+++ b/src/LibNftables/NftTable.cs
@@ -0,0 +1,22 @@
+namespace LibNftables;
+
+///
+/// Represents a typed nftables table definition.
+///
+public sealed class NftTable
+{
+ ///
+ /// Gets or sets the nftables family for the table.
+ ///
+ public NftFamily Family { get; set; } = NftFamily.Inet;
+
+ ///
+ /// Gets or sets the table name.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Gets the sets declared in this table.
+ ///
+ public IList Sets { get; } = new List();
+}
diff --git a/src/LibNftables/NftablesClient.cs b/src/LibNftables/NftablesClient.cs
index c589ec1..a7027bf 100644
--- a/src/LibNftables/NftablesClient.cs
+++ b/src/LibNftables/NftablesClient.cs
@@ -56,10 +56,21 @@ public sealed class NftablesClient : INftablesClient
}
}
+ ///
+ public NftValidationResult ValidateRuleset(NftRuleset ruleset, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+ return Validate(CreateTypedRequest(ruleset), ct);
+ }
+
///
public Task ValidateAsync(NftApplyRequest request, CancellationToken ct = default)
=> Task.FromResult(Validate(request, ct));
+ ///
+ public Task ValidateRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
+ => Task.FromResult(ValidateRuleset(ruleset, ct));
+
///
public void Apply(NftApplyRequest request, CancellationToken ct = default)
{
@@ -68,6 +79,13 @@ public sealed class NftablesClient : INftablesClient
_ = Execute(request, forceDryRun: null, ct);
}
+ ///
+ public void ApplyRuleset(NftRuleset ruleset, CancellationToken ct = default)
+ {
+ ct.ThrowIfCancellationRequested();
+ Apply(CreateTypedRequest(ruleset), ct);
+ }
+
///
public Task ApplyAsync(NftApplyRequest request, CancellationToken ct = default)
{
@@ -75,6 +93,13 @@ public sealed class NftablesClient : INftablesClient
return Task.CompletedTask;
}
+ ///
+ public Task ApplyRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
+ {
+ ApplyRuleset(ruleset, ct);
+ return Task.CompletedTask;
+ }
+
///
public NftSnapshot Snapshot(CancellationToken ct = default)
{
@@ -210,5 +235,8 @@ public sealed class NftablesClient : INftablesClient
return false;
}
+ private static NftApplyRequest CreateTypedRequest(NftRuleset ruleset)
+ => NftApplyRequest.FromText(NftRulesetRenderer.Render(ruleset), dryRun: false);
+
private sealed record CommandExecutionResult(string? Output, string? Error);
}
diff --git a/tests/LibNftables.Tests/NftRulesetRendererTests.cs b/tests/LibNftables.Tests/NftRulesetRendererTests.cs
new file mode 100644
index 0000000..3a0d889
--- /dev/null
+++ b/tests/LibNftables.Tests/NftRulesetRendererTests.cs
@@ -0,0 +1,99 @@
+namespace LibNftables.Tests;
+
+public sealed class NftRulesetRendererTests
+{
+ [Fact]
+ public void Render_WithTypedSet_RendersExpectedCommands()
+ {
+ var ruleset = new NftRuleset();
+ var table = new NftTable
+ {
+ Family = NftFamily.Inet,
+ Name = "filter",
+ };
+ var set = new NftSet
+ {
+ Name = "blocked_ipv4",
+ Type = NftSetType.Ipv4Address,
+ };
+ set.Elements.Add("10.0.0.1");
+ set.Elements.Add("10.0.0.2");
+ table.Sets.Add(set);
+ ruleset.Tables.Add(table);
+
+ string rendered = NftRulesetRenderer.Render(ruleset);
+
+ Assert.Equal(
+ "add table inet filter" + Environment.NewLine +
+ "add set inet filter blocked_ipv4 { type ipv4_addr; elements = { 10.0.0.1, 10.0.0.2 }; }" + Environment.NewLine,
+ rendered);
+ }
+
+ [Fact]
+ public void Render_WithCustomTypeExpression_UsesCustomKeyword()
+ {
+ var ruleset = new NftRuleset();
+ var table = new NftTable
+ {
+ Family = NftFamily.Ip,
+ Name = "custom",
+ };
+ var set = new NftSet
+ {
+ Name = "ports",
+ CustomTypeExpression = "inet_service",
+ };
+ set.Elements.Add("80");
+ table.Sets.Add(set);
+ ruleset.Tables.Add(table);
+
+ string rendered = NftRulesetRenderer.Render(ruleset);
+
+ Assert.Contains("type inet_service;", rendered, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Render_WithNoTables_ThrowsValidationException()
+ {
+ Assert.Throws(() => NftRulesetRenderer.Render(new NftRuleset()));
+ }
+
+ [Fact]
+ public void Render_WithConflictingSetTypeSources_ThrowsValidationException()
+ {
+ var ruleset = new NftRuleset();
+ var table = new NftTable
+ {
+ Name = "filter",
+ };
+ table.Sets.Add(new NftSet
+ {
+ Name = "conflict",
+ Type = NftSetType.Ipv4Address,
+ CustomTypeExpression = "ipv4_addr",
+ });
+ ruleset.Tables.Add(table);
+
+ Assert.Throws(() => NftRulesetRenderer.Render(ruleset));
+ }
+
+ [Fact]
+ public void Render_WithInvalidElement_ThrowsValidationException()
+ {
+ var ruleset = new NftRuleset();
+ var table = new NftTable
+ {
+ Name = "filter",
+ };
+ var set = new NftSet
+ {
+ Name = "bad",
+ Type = NftSetType.Mark,
+ };
+ set.Elements.Add("1; drop");
+ table.Sets.Add(set);
+ ruleset.Tables.Add(table);
+
+ Assert.Throws(() => NftRulesetRenderer.Render(ruleset));
+ }
+}
diff --git a/tests/LibNftables.Tests/NftablesClientIntegrationTests.cs b/tests/LibNftables.Tests/NftablesClientIntegrationTests.cs
index 87c3752..4b1196b 100644
--- a/tests/LibNftables.Tests/NftablesClientIntegrationTests.cs
+++ b/tests/LibNftables.Tests/NftablesClientIntegrationTests.cs
@@ -71,6 +71,36 @@ public sealed class NftablesClientIntegrationTests
}
}
+ [Fact]
+ public void ValidateRuleset_WithTypedSetDefinition_ReturnsValidResult()
+ {
+ if (!CanCreateClient())
+ {
+ return;
+ }
+
+ var client = new NftablesClient();
+ var ruleset = new NftRuleset();
+ var table = new NftTable
+ {
+ Family = NftFamily.Inet,
+ Name = "typed_validation",
+ };
+ var set = new NftSet
+ {
+ Name = "blocked_ipv4",
+ Type = NftSetType.Ipv4Address,
+ };
+ set.Elements.Add("10.0.0.1");
+ set.Elements.Add("10.0.0.2");
+ table.Sets.Add(set);
+ ruleset.Tables.Add(table);
+
+ NftValidationResult result = client.ValidateRuleset(ruleset);
+
+ Assert.True(result.IsValid);
+ }
+
private static bool CanCreateClient()
{
try
diff --git a/tests/LibNftables.Tests/NftablesClientUnitTests.cs b/tests/LibNftables.Tests/NftablesClientUnitTests.cs
index 848fd13..8ded37e 100644
--- a/tests/LibNftables.Tests/NftablesClientUnitTests.cs
+++ b/tests/LibNftables.Tests/NftablesClientUnitTests.cs
@@ -64,6 +64,55 @@ public sealed class NftablesClientUnitTests
Assert.Equal("ok", result.Output);
}
+ [Fact]
+ public void ValidateRuleset_RendersTypedRulesetAndUsesDryRun()
+ {
+ var context = new FakeContext
+ {
+ OutputBuffer = "ok",
+ ErrorBuffer = string.Empty,
+ };
+ var client = CreateClient(() => context);
+
+ NftValidationResult result = client.ValidateRuleset(CreateTypedRuleset());
+
+ Assert.True(result.IsValid);
+ Assert.True(context.DryRun);
+ Assert.Equal(
+ "add table inet filter" + Environment.NewLine +
+ "add set inet filter blocked_ipv4 { type ipv4_addr; elements = { 10.0.0.1, 10.0.0.2 }; }" + Environment.NewLine,
+ context.LastCommandText);
+ }
+
+ [Fact]
+ public void ApplyRuleset_RendersTypedRulesetAndExecutesCommand()
+ {
+ var context = new FakeContext();
+ var client = CreateClient(() => context);
+
+ client.ApplyRuleset(CreateTypedRuleset());
+
+ Assert.False(context.DryRun);
+ Assert.Equal(
+ "add table inet filter" + Environment.NewLine +
+ "add set inet filter blocked_ipv4 { type ipv4_addr; elements = { 10.0.0.1, 10.0.0.2 }; }" + Environment.NewLine,
+ context.LastCommandText);
+ }
+
+ [Fact]
+ public async Task ApplyRulesetAsync_UsesTypedRulesetText()
+ {
+ var context = new FakeContext();
+ var client = CreateClient(() => context);
+
+ await client.ApplyRulesetAsync(CreateTypedRuleset());
+
+ Assert.Equal(
+ "add table inet filter" + Environment.NewLine +
+ "add set inet filter blocked_ipv4 { type ipv4_addr; elements = { 10.0.0.1, 10.0.0.2 }; }" + Environment.NewLine,
+ context.LastCommandText);
+ }
+
[Fact]
public void Apply_WithFileRequest_UsesFileExecutionPath()
{
@@ -191,6 +240,26 @@ public sealed class NftablesClientUnitTests
NftablesClientOptions? options = null)
=> new(options, contextFactory ?? (() => new FakeContext()), skipRuntimeGuard: true);
+ private static NftRuleset CreateTypedRuleset()
+ {
+ var ruleset = new NftRuleset();
+ var table = new NftTable
+ {
+ Family = NftFamily.Inet,
+ Name = "filter",
+ };
+ var set = new NftSet
+ {
+ Name = "blocked_ipv4",
+ Type = NftSetType.Ipv4Address,
+ };
+ set.Elements.Add("10.0.0.1");
+ set.Elements.Add("10.0.0.2");
+ table.Sets.Add(set);
+ ruleset.Tables.Add(table);
+ return ruleset;
+ }
+
private sealed class FakeContext : INftContextHandle
{
public bool DryRun { get; set; }