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
|
||||||
|
|
||||||
`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
|
## Current Scope
|
||||||
|
|
||||||
This library is intentionally narrow.
|
This library is intentionally narrow.
|
||||||
|
|
||||||
- High-level managed API:
|
- High-level managed API:
|
||||||
- `Validate`
|
- typed `NftRuleset` / `NftTable` / `NftSet` authoring
|
||||||
- `Apply`
|
- `ValidateRuleset`
|
||||||
|
- `ApplyRuleset`
|
||||||
- `Snapshot`
|
- `Snapshot`
|
||||||
- `Restore`
|
- `Restore`
|
||||||
- Low-level managed wrapper:
|
- Low-level managed wrapper:
|
||||||
@@ -16,7 +17,7 @@ This library is intentionally narrow.
|
|||||||
|
|
||||||
Non-goals for the current release:
|
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
|
- Event monitoring or subscriptions
|
||||||
- Cross-platform support beyond Linux x64
|
- Cross-platform support beyond Linux x64
|
||||||
|
|
||||||
@@ -67,13 +68,31 @@ using LibNftables;
|
|||||||
|
|
||||||
INftablesClient client = new NftablesClient();
|
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)
|
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
|
## Low-Level Example
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -113,7 +132,7 @@ Some operations require elevated privileges or `CAP_NET_ADMIN`, especially when
|
|||||||
|
|
||||||
### Validation failures
|
### 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
|
## Bindings and Regeneration
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ public interface INftablesClient
|
|||||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
||||||
NftValidationResult Validate(NftApplyRequest request, System.Threading.CancellationToken ct = default);
|
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>
|
/// <summary>
|
||||||
/// Asynchronously validates a ruleset request using dry-run mode.
|
/// Asynchronously validates a ruleset request using dry-run mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -29,6 +42,17 @@ public interface INftablesClient
|
|||||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
/// <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);
|
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>
|
/// <summary>
|
||||||
/// Applies a ruleset request.
|
/// Applies a ruleset request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -41,6 +65,18 @@ public interface INftablesClient
|
|||||||
/// <exception cref="NftException">Thrown for other native execution failures.</exception>
|
/// <exception cref="NftException">Thrown for other native execution failures.</exception>
|
||||||
void Apply(NftApplyRequest request, System.Threading.CancellationToken ct = default);
|
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>
|
/// <summary>
|
||||||
/// Asynchronously applies a ruleset request.
|
/// Asynchronously applies a ruleset request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -54,6 +90,19 @@ public interface INftablesClient
|
|||||||
/// <exception cref="NftException">Thrown for other native execution failures.</exception>
|
/// <exception cref="NftException">Thrown for other native execution failures.</exception>
|
||||||
System.Threading.Tasks.Task ApplyAsync(NftApplyRequest request, System.Threading.CancellationToken ct = default);
|
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>
|
/// <summary>
|
||||||
/// Captures the current nftables ruleset from the system.
|
/// Captures the current nftables ruleset from the system.
|
||||||
/// </summary>
|
/// </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 />
|
/// <inheritdoc />
|
||||||
public Task<NftValidationResult> ValidateAsync(NftApplyRequest request, CancellationToken ct = default)
|
public Task<NftValidationResult> ValidateAsync(NftApplyRequest request, CancellationToken ct = default)
|
||||||
=> Task.FromResult(Validate(request, ct));
|
=> Task.FromResult(Validate(request, ct));
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<NftValidationResult> ValidateRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
||||||
|
=> Task.FromResult(ValidateRuleset(ruleset, ct));
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Apply(NftApplyRequest request, CancellationToken ct = default)
|
public void Apply(NftApplyRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -68,6 +79,13 @@ public sealed class NftablesClient : INftablesClient
|
|||||||
_ = Execute(request, forceDryRun: null, ct);
|
_ = Execute(request, forceDryRun: null, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ApplyRuleset(NftRuleset ruleset, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
Apply(CreateTypedRequest(ruleset), ct);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task ApplyAsync(NftApplyRequest request, CancellationToken ct = default)
|
public Task ApplyAsync(NftApplyRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -75,6 +93,13 @@ public sealed class NftablesClient : INftablesClient
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task ApplyRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ApplyRuleset(ruleset, ct);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public NftSnapshot Snapshot(CancellationToken ct = default)
|
public NftSnapshot Snapshot(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -210,5 +235,8 @@ public sealed class NftablesClient : INftablesClient
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static NftApplyRequest CreateTypedRequest(NftRuleset ruleset)
|
||||||
|
=> NftApplyRequest.FromText(NftRulesetRenderer.Render(ruleset), dryRun: false);
|
||||||
|
|
||||||
private sealed record CommandExecutionResult(string? Output, string? Error);
|
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()
|
private static bool CanCreateClient()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -64,6 +64,55 @@ public sealed class NftablesClientUnitTests
|
|||||||
Assert.Equal("ok", result.Output);
|
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]
|
[Fact]
|
||||||
public void Apply_WithFileRequest_UsesFileExecutionPath()
|
public void Apply_WithFileRequest_UsesFileExecutionPath()
|
||||||
{
|
{
|
||||||
@@ -191,6 +240,26 @@ public sealed class NftablesClientUnitTests
|
|||||||
NftablesClientOptions? options = null)
|
NftablesClientOptions? options = null)
|
||||||
=> new(options, contextFactory ?? (() => new FakeContext()), skipRuntimeGuard: true);
|
=> 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
|
private sealed class FakeContext : INftContextHandle
|
||||||
{
|
{
|
||||||
public bool DryRun { get; set; }
|
public bool DryRun { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user