Add typed high-level ruleset and set API
This commit is contained in:
33
README.md
33
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
|
||||
|
||||
|
||||
@@ -18,6 +18,19 @@ public interface INftablesClient
|
||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
||||
NftValidationResult Validate(NftApplyRequest request, System.Threading.CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a typed nftables ruleset by rendering it to nft command text in dry-run mode.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The typed ruleset to validate.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A validation result. Invalid nft syntax or schema errors return <see cref="NftValidationResult.IsValid"/> = <see langword="false"/>.
|
||||
/// </returns>
|
||||
/// <exception cref="NftValidationException">Thrown when the typed model itself is invalid before native validation begins.</exception>
|
||||
/// <exception cref="NftUnsupportedException">Thrown when runtime/platform is unsupported.</exception>
|
||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
||||
NftValidationResult ValidateRuleset(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously validates a ruleset request using dry-run mode.
|
||||
/// </summary>
|
||||
@@ -29,6 +42,17 @@ public interface INftablesClient
|
||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
||||
System.Threading.Tasks.Task<NftValidationResult> ValidateAsync(NftApplyRequest request, System.Threading.CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously validates a typed nftables ruleset by rendering it to nft command text in dry-run mode.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The typed ruleset to validate.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <returns>A task that resolves to a validation result.</returns>
|
||||
/// <exception cref="NftValidationException">Thrown when the typed model itself is invalid before native validation begins.</exception>
|
||||
/// <exception cref="NftUnsupportedException">Thrown when runtime/platform is unsupported.</exception>
|
||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
||||
System.Threading.Tasks.Task<NftValidationResult> ValidateRulesetAsync(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a ruleset request.
|
||||
/// </summary>
|
||||
@@ -41,6 +65,18 @@ public interface INftablesClient
|
||||
/// <exception cref="NftException">Thrown for other native execution failures.</exception>
|
||||
void Apply(NftApplyRequest request, System.Threading.CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a typed nftables ruleset by rendering it to nft command text.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The typed ruleset to apply.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <exception cref="NftValidationException">Thrown when the typed model itself is invalid or the rendered ruleset cannot be parsed/validated.</exception>
|
||||
/// <exception cref="NftPermissionException">Thrown when insufficient privileges are available for runtime operation.</exception>
|
||||
/// <exception cref="NftUnsupportedException">Thrown when runtime/platform is unsupported.</exception>
|
||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
||||
/// <exception cref="NftException">Thrown for other native execution failures.</exception>
|
||||
void ApplyRuleset(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously applies a ruleset request.
|
||||
/// </summary>
|
||||
@@ -54,6 +90,19 @@ public interface INftablesClient
|
||||
/// <exception cref="NftException">Thrown for other native execution failures.</exception>
|
||||
System.Threading.Tasks.Task ApplyAsync(NftApplyRequest request, System.Threading.CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously applies a typed nftables ruleset by rendering it to nft command text.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The typed ruleset to apply.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <returns>A completed task when apply succeeds.</returns>
|
||||
/// <exception cref="NftValidationException">Thrown when the typed model itself is invalid or the rendered ruleset cannot be parsed/validated.</exception>
|
||||
/// <exception cref="NftPermissionException">Thrown when insufficient privileges are available for runtime operation.</exception>
|
||||
/// <exception cref="NftUnsupportedException">Thrown when runtime/platform is unsupported.</exception>
|
||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
||||
/// <exception cref="NftException">Thrown for other native execution failures.</exception>
|
||||
System.Threading.Tasks.Task ApplyRulesetAsync(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current nftables ruleset from the system.
|
||||
/// </summary>
|
||||
|
||||
37
src/LibNftables/NftFamily.cs
Normal file
37
src/LibNftables/NftFamily.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace LibNftables;
|
||||
|
||||
/// <summary>
|
||||
/// Supported nftables address families for typed ruleset authoring.
|
||||
/// </summary>
|
||||
public enum NftFamily
|
||||
{
|
||||
/// <summary>
|
||||
/// The <c>inet</c> family.
|
||||
/// </summary>
|
||||
Inet,
|
||||
|
||||
/// <summary>
|
||||
/// The <c>ip</c> family.
|
||||
/// </summary>
|
||||
Ip,
|
||||
|
||||
/// <summary>
|
||||
/// The <c>ip6</c> family.
|
||||
/// </summary>
|
||||
Ip6,
|
||||
|
||||
/// <summary>
|
||||
/// The <c>arp</c> family.
|
||||
/// </summary>
|
||||
Arp,
|
||||
|
||||
/// <summary>
|
||||
/// The <c>bridge</c> family.
|
||||
/// </summary>
|
||||
Bridge,
|
||||
|
||||
/// <summary>
|
||||
/// The <c>netdev</c> family.
|
||||
/// </summary>
|
||||
Netdev,
|
||||
}
|
||||
12
src/LibNftables/NftRuleset.cs
Normal file
12
src/LibNftables/NftRuleset.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace LibNftables;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a typed nftables ruleset authored through the high-level API.
|
||||
/// </summary>
|
||||
public sealed class NftRuleset
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the tables included in this ruleset.
|
||||
/// </summary>
|
||||
public IList<NftTable> Tables { get; } = new List<NftTable>();
|
||||
}
|
||||
198
src/LibNftables/NftRulesetRenderer.cs
Normal file
198
src/LibNftables/NftRulesetRenderer.cs
Normal file
@@ -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<string>(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<string>(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),
|
||||
};
|
||||
}
|
||||
37
src/LibNftables/NftSet.cs
Normal file
37
src/LibNftables/NftSet.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace LibNftables;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a typed nftables set definition backed by native C# collections.
|
||||
/// </summary>
|
||||
public sealed class NftSet
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the set name.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a common typed set element type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Set either <see cref="Type"/> or <see cref="CustomTypeExpression"/>.
|
||||
/// </remarks>
|
||||
public NftSetType? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a custom nftables type expression for the set.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This exists as a fallback for types not yet modeled by <see cref="NftSetType"/>.
|
||||
/// Set either <see cref="Type"/> or <see cref="CustomTypeExpression"/>.
|
||||
/// </remarks>
|
||||
public string? CustomTypeExpression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set elements to declare inline with the set definition.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Values are rendered as nftables literals and should already be valid for the selected type.
|
||||
/// </remarks>
|
||||
public IList<string> Elements { get; } = new List<string>();
|
||||
}
|
||||
37
src/LibNftables/NftSetType.cs
Normal file
37
src/LibNftables/NftSetType.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace LibNftables;
|
||||
|
||||
/// <summary>
|
||||
/// Common typed nftables set element types supported by the high-level API.
|
||||
/// </summary>
|
||||
public enum NftSetType
|
||||
{
|
||||
/// <summary>
|
||||
/// IPv4 addresses (<c>ipv4_addr</c>).
|
||||
/// </summary>
|
||||
Ipv4Address,
|
||||
|
||||
/// <summary>
|
||||
/// IPv6 addresses (<c>ipv6_addr</c>).
|
||||
/// </summary>
|
||||
Ipv6Address,
|
||||
|
||||
/// <summary>
|
||||
/// Internet service/port values (<c>inet_service</c>).
|
||||
/// </summary>
|
||||
InetService,
|
||||
|
||||
/// <summary>
|
||||
/// Ethernet/MAC addresses (<c>ether_addr</c>).
|
||||
/// </summary>
|
||||
EtherAddress,
|
||||
|
||||
/// <summary>
|
||||
/// Interface names (<c>ifname</c>).
|
||||
/// </summary>
|
||||
InterfaceName,
|
||||
|
||||
/// <summary>
|
||||
/// Packet marks (<c>mark</c>).
|
||||
/// </summary>
|
||||
Mark,
|
||||
}
|
||||
22
src/LibNftables/NftTable.cs
Normal file
22
src/LibNftables/NftTable.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace LibNftables;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a typed nftables table definition.
|
||||
/// </summary>
|
||||
public sealed class NftTable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the nftables family for the table.
|
||||
/// </summary>
|
||||
public NftFamily Family { get; set; } = NftFamily.Inet;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the table name.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sets declared in this table.
|
||||
/// </summary>
|
||||
public IList<NftSet> Sets { get; } = new List<NftSet>();
|
||||
}
|
||||
@@ -56,10 +56,21 @@ public sealed class NftablesClient : INftablesClient
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NftValidationResult ValidateRuleset(NftRuleset ruleset, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Validate(CreateTypedRequest(ruleset), ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<NftValidationResult> ValidateAsync(NftApplyRequest request, CancellationToken ct = default)
|
||||
=> Task.FromResult(Validate(request, ct));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<NftValidationResult> ValidateRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
||||
=> Task.FromResult(ValidateRuleset(ruleset, ct));
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(NftApplyRequest request, CancellationToken ct = default)
|
||||
{
|
||||
@@ -68,6 +79,13 @@ public sealed class NftablesClient : INftablesClient
|
||||
_ = Execute(request, forceDryRun: null, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyRuleset(NftRuleset ruleset, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
Apply(CreateTypedRequest(ruleset), ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ApplyAsync(NftApplyRequest request, CancellationToken ct = default)
|
||||
{
|
||||
@@ -75,6 +93,13 @@ public sealed class NftablesClient : INftablesClient
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ApplyRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
||||
{
|
||||
ApplyRuleset(ruleset, ct);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
99
tests/LibNftables.Tests/NftRulesetRendererTests.cs
Normal file
99
tests/LibNftables.Tests/NftRulesetRendererTests.cs
Normal file
@@ -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<NftValidationException>(() => 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<NftValidationException>(() => 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<NftValidationException>(() => NftRulesetRenderer.Render(ruleset));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user