Complete typed map ergonomics and preview API
This commit is contained in:
52
README.md
52
README.md
@@ -9,6 +9,7 @@ This library is intentionally narrow.
|
|||||||
- High-level managed API:
|
- High-level managed API:
|
||||||
- typed `NftRuleset` / `NftTable` / `NftSet` / `NftMap` / `NftChain` / `NftRule` authoring
|
- typed `NftRuleset` / `NftTable` / `NftSet` / `NftMap` / `NftChain` / `NftRule` authoring
|
||||||
- `RenderRuleset`
|
- `RenderRuleset`
|
||||||
|
- `ValidateAndRenderRuleset`
|
||||||
- `ValidateRuleset`
|
- `ValidateRuleset`
|
||||||
- `ApplyRuleset`
|
- `ApplyRuleset`
|
||||||
- `Snapshot`
|
- `Snapshot`
|
||||||
@@ -114,6 +115,55 @@ if (validation.IsValid)
|
|||||||
|
|
||||||
Raw command text remains available through `NftApplyRequest` as a fallback for nft syntax not yet modeled by the typed API.
|
Raw command text remains available through `NftApplyRequest` as a fallback for nft syntax not yet modeled by the typed API.
|
||||||
|
|
||||||
|
## Supported Typed Subset
|
||||||
|
|
||||||
|
The typed API currently supports this subset directly:
|
||||||
|
|
||||||
|
- Sets with typed values for IPv4/IPv6 addresses and CIDRs, ports/services, interface names, marks, and raw literals.
|
||||||
|
- Maps declared and populated inline, authored through dictionary-style helpers on `NftMap`.
|
||||||
|
- Chains with typed base-chain metadata: type, hook, priority, and policy.
|
||||||
|
- Common firewall rules:
|
||||||
|
- source/destination address matches
|
||||||
|
- source/destination port matches
|
||||||
|
- input/output interface matches
|
||||||
|
- set membership matches
|
||||||
|
- verdicts: accept, drop, reject, jump
|
||||||
|
|
||||||
|
Use `NftApplyRequest` as the fallback when you need:
|
||||||
|
|
||||||
|
- nft features outside the current typed subset
|
||||||
|
- live map mutation commands
|
||||||
|
- custom map key/value type expressions with non-raw typed values
|
||||||
|
- snapshot parsing back into typed object models
|
||||||
|
- full nft expression parity
|
||||||
|
|
||||||
|
## Map Authoring Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var map = new NftMap
|
||||||
|
{
|
||||||
|
Name = "service_policy",
|
||||||
|
KeyType = NftMapType.InetService,
|
||||||
|
ValueType = NftMapType.Verdict,
|
||||||
|
};
|
||||||
|
|
||||||
|
map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Accept));
|
||||||
|
map.Add(NftValue.Port(443), NftValue.Verdict(NftVerdict.Drop));
|
||||||
|
```
|
||||||
|
|
||||||
|
Entries are rendered in insertion order. Duplicate keys are rejected by `NftMap.Add`. Use `NftMap.Set` to replace an existing key without changing its order.
|
||||||
|
|
||||||
|
## Validate-And-Render Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
NftRenderedValidationResult preview = client.ValidateAndRenderRuleset(ruleset);
|
||||||
|
|
||||||
|
if (preview.ValidationResult.IsValid)
|
||||||
|
{
|
||||||
|
client.ApplyRuleset(ruleset);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Low-Level Example
|
## Low-Level Example
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -153,7 +203,7 @@ Some operations require elevated privileges or `CAP_NET_ADMIN`, especially when
|
|||||||
|
|
||||||
### Validation failures
|
### Validation failures
|
||||||
|
|
||||||
`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.
|
`ValidateRuleset` and `ValidateAndRenderRuleset` return `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
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,17 @@ public interface INftablesClient
|
|||||||
/// <exception cref="NftValidationException">Thrown when the typed model itself is invalid.</exception>
|
/// <exception cref="NftValidationException">Thrown when the typed model itself is invalid.</exception>
|
||||||
string RenderRuleset(NftRuleset ruleset);
|
string RenderRuleset(NftRuleset ruleset);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders and validates a typed nftables ruleset in one flow.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ruleset">The typed ruleset to render and validate.</param>
|
||||||
|
/// <param name="ct">A cancellation token.</param>
|
||||||
|
/// <returns>The rendered text and validation outcome.</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>
|
||||||
|
NftRenderedValidationResult ValidateAndRenderRuleset(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>
|
||||||
@@ -61,6 +72,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> ValidateRulesetAsync(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
System.Threading.Tasks.Task<NftValidationResult> ValidateRulesetAsync(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously renders and validates a typed nftables ruleset in one flow.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ruleset">The typed ruleset to render and validate.</param>
|
||||||
|
/// <param name="ct">A cancellation token.</param>
|
||||||
|
/// <returns>A task that resolves to the rendered text and validation outcome.</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<NftRenderedValidationResult> ValidateAndRenderRulesetAsync(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies a ruleset request.
|
/// Applies a ruleset request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ namespace LibNftables;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class NftMap
|
public sealed class NftMap
|
||||||
{
|
{
|
||||||
|
private readonly List<NftMapEntry> _entries = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the map name.
|
/// Gets or sets the map name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -43,7 +45,133 @@ public sealed class NftMap
|
|||||||
public string? CustomValueTypeExpression { get; set; }
|
public string? CustomValueTypeExpression { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the entries declared inline with the map definition.
|
/// Gets the number of entries currently stored in the map.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<NftMapEntry> Entries { get; } = new List<NftMapEntry>();
|
public int Count => _entries.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the entries declared inline with the map definition in insertion order.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<NftMapEntry> Entries => _entries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a new entry to the map.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The entry key.</param>
|
||||||
|
/// <param name="value">The entry value.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> or <paramref name="value"/> is null.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when an entry with the same rendered key already exists.</exception>
|
||||||
|
public void Add(NftValue key, NftValue value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
|
||||||
|
if (ContainsKey(key))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Map already contains an entry for key '{key.RenderedText}'.", nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
_entries.Add(new NftMapEntry
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
Value = value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds or replaces an entry in the map while preserving insertion order for existing keys.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The entry key.</param>
|
||||||
|
/// <param name="value">The entry value.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> or <paramref name="value"/> is null.</exception>
|
||||||
|
public void Set(NftValue key, NftValue value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
|
||||||
|
int existingIndex = FindIndex(key);
|
||||||
|
if (existingIndex >= 0)
|
||||||
|
{
|
||||||
|
_entries[existingIndex].Value = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the map contains an entry for the specified key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The entry key.</param>
|
||||||
|
/// <returns><see langword="true"/> when the key exists; otherwise <see langword="false"/>.</returns>
|
||||||
|
public bool ContainsKey(NftValue key)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
return FindIndex(key) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve the value for the specified key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The entry key.</param>
|
||||||
|
/// <param name="value">The matching value when found.</param>
|
||||||
|
/// <returns><see langword="true"/> when the key exists; otherwise <see langword="false"/>.</returns>
|
||||||
|
public bool TryGetValue(NftValue key, out NftValue value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
|
||||||
|
int index = FindIndex(key);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
value = _entries[index].Value!;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the entry for the specified key when present.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The entry key.</param>
|
||||||
|
/// <returns><see langword="true"/> when an entry was removed; otherwise <see langword="false"/>.</returns>
|
||||||
|
public bool Remove(NftValue key)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
|
||||||
|
int index = FindIndex(key);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_entries.RemoveAt(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all entries from the map.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear() => _entries.Clear();
|
||||||
|
|
||||||
|
private int FindIndex(NftValue key)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _entries.Count; i++)
|
||||||
|
{
|
||||||
|
NftMapEntry entry = _entries[i];
|
||||||
|
if (entry.Key is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Key.Kind == key.Kind &&
|
||||||
|
string.Equals(entry.Key.RenderedText, key.RenderedText, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/LibNftables/NftRenderedValidationResult.cs
Normal file
8
src/LibNftables/NftRenderedValidationResult.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the rendered nft command text and validation outcome for a typed ruleset preview.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="RenderedRulesetText">The rendered nft command text.</param>
|
||||||
|
/// <param name="ValidationResult">The validation outcome for the rendered ruleset.</param>
|
||||||
|
public sealed record NftRenderedValidationResult(string RenderedRulesetText, NftValidationResult ValidationResult);
|
||||||
@@ -404,8 +404,14 @@ internal static class NftRulesetRenderer
|
|||||||
throw new NftValidationException($"Map '{map.Name}' contains a null entry at index {i}.", 0, null);
|
throw new NftValidationException($"Map '{map.Name}' contains a null entry at index {i}.", 0, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
string renderedKey = RenderValue(entry.Key!, $"Map '{map.Name}' key");
|
NftValue key = entry.Key ?? throw new NftValidationException($"Map '{map.Name}' contains a null key at index {i}.", 0, null);
|
||||||
_ = RenderValue(entry.Value!, $"Map '{map.Name}' value");
|
NftValue value = entry.Value ?? throw new NftValidationException($"Map '{map.Name}' contains a null value at index {i}.", 0, null);
|
||||||
|
|
||||||
|
string renderedKey = RenderValue(key, $"Map '{map.Name}' key");
|
||||||
|
_ = RenderValue(value, $"Map '{map.Name}' value");
|
||||||
|
|
||||||
|
ValidateMapEntryValueCompatibility(map, key, isKey: true);
|
||||||
|
ValidateMapEntryValueCompatibility(map, value, isKey: false);
|
||||||
|
|
||||||
if (!keys.Add(renderedKey))
|
if (!keys.Add(renderedKey))
|
||||||
{
|
{
|
||||||
@@ -414,6 +420,84 @@ internal static class NftRulesetRenderer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ValidateMapEntryValueCompatibility(NftMap map, NftValue value, bool isKey)
|
||||||
|
{
|
||||||
|
string role = isKey ? "key" : "value";
|
||||||
|
NftMapType? typedType = isKey ? map.KeyType : map.ValueType;
|
||||||
|
string? customType = isKey ? map.CustomKeyTypeExpression : map.CustomValueTypeExpression;
|
||||||
|
|
||||||
|
if (!typedType.HasValue)
|
||||||
|
{
|
||||||
|
if (value.Kind != NftValueKind.Raw)
|
||||||
|
{
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Map '{map.Name}' uses custom {role} type '{customType}', so only raw values are supported for that {role}.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typedType.Value)
|
||||||
|
{
|
||||||
|
case NftMapType.Ipv4Address:
|
||||||
|
ValidateTypedMapAddressValue(map, value, role, AddressFamily.InterNetwork);
|
||||||
|
return;
|
||||||
|
case NftMapType.Ipv6Address:
|
||||||
|
ValidateTypedMapAddressValue(map, value, role, AddressFamily.InterNetworkV6);
|
||||||
|
return;
|
||||||
|
case NftMapType.InetService:
|
||||||
|
ValidateTypedMapValueKind(map, value, role, NftValueKind.Port, NftValueKind.Raw);
|
||||||
|
return;
|
||||||
|
case NftMapType.InterfaceName:
|
||||||
|
ValidateTypedMapValueKind(map, value, role, NftValueKind.Interface, NftValueKind.Raw);
|
||||||
|
return;
|
||||||
|
case NftMapType.Mark:
|
||||||
|
ValidateTypedMapValueKind(map, value, role, NftValueKind.Mark, NftValueKind.Raw);
|
||||||
|
return;
|
||||||
|
case NftMapType.Verdict:
|
||||||
|
ValidateTypedMapValueKind(map, value, role, NftValueKind.Verdict, NftValueKind.Raw);
|
||||||
|
return;
|
||||||
|
case NftMapType.EtherAddress:
|
||||||
|
ValidateTypedMapValueKind(map, value, role, NftValueKind.Raw);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new NftValidationException($"Map '{map.Name}' uses unsupported typed {role} type '{typedType}'.", 0, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateTypedMapAddressValue(NftMap map, NftValue value, string role, AddressFamily family)
|
||||||
|
{
|
||||||
|
if (value.Kind == NftValueKind.Raw)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateTypedMapValueKind(map, value, role, NftValueKind.Address, NftValueKind.Network);
|
||||||
|
if (value.AddressFamily != family)
|
||||||
|
{
|
||||||
|
string familyName = family == AddressFamily.InterNetwork ? "IPv4" : "IPv6";
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Map '{map.Name}' {role} '{value.RenderedText}' is not compatible with the declared {familyName} map type.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateTypedMapValueKind(NftMap map, NftValue value, string role, params NftValueKind[] allowedKinds)
|
||||||
|
{
|
||||||
|
if (Array.IndexOf(allowedKinds, value.Kind) >= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Map '{map.Name}' {role} '{value.RenderedText}' is not compatible with the declared map type.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
private static void ValidateChain(NftChain chain, NftTable table)
|
private static void ValidateChain(NftChain chain, NftTable table)
|
||||||
{
|
{
|
||||||
if (chain is null)
|
if (chain is null)
|
||||||
|
|||||||
@@ -60,13 +60,23 @@ public sealed class NftablesClient : INftablesClient
|
|||||||
public NftValidationResult ValidateRuleset(NftRuleset ruleset, CancellationToken ct = default)
|
public NftValidationResult ValidateRuleset(NftRuleset ruleset, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
return Validate(CreateTypedRequest(ruleset), ct);
|
return ValidateAndRenderRuleset(ruleset, ct).ValidationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string RenderRuleset(NftRuleset ruleset)
|
public string RenderRuleset(NftRuleset ruleset)
|
||||||
=> NftRulesetRenderer.Render(ruleset);
|
=> NftRulesetRenderer.Render(ruleset);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public NftRenderedValidationResult ValidateAndRenderRuleset(NftRuleset ruleset, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
string rendered = RenderRuleset(ruleset);
|
||||||
|
NftValidationResult validation = Validate(NftApplyRequest.FromText(rendered), ct);
|
||||||
|
return new NftRenderedValidationResult(rendered, validation);
|
||||||
|
}
|
||||||
|
|
||||||
/// <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));
|
||||||
@@ -75,6 +85,10 @@ public sealed class NftablesClient : INftablesClient
|
|||||||
public Task<NftValidationResult> ValidateRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
public Task<NftValidationResult> ValidateRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
||||||
=> Task.FromResult(ValidateRuleset(ruleset, ct));
|
=> Task.FromResult(ValidateRuleset(ruleset, ct));
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<NftRenderedValidationResult> ValidateAndRenderRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
||||||
|
=> Task.FromResult(ValidateAndRenderRuleset(ruleset, ct));
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Apply(NftApplyRequest request, CancellationToken ct = default)
|
public void Apply(NftApplyRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,27 @@ public sealed class NftRulesetRendererTests
|
|||||||
Assert.Contains("443 : drop", rendered, StringComparison.Ordinal);
|
Assert.Contains("443 : drop", rendered, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Render_WithIncompatibleTypedMapKey_ThrowsValidationException()
|
||||||
|
{
|
||||||
|
var ruleset = new NftRuleset();
|
||||||
|
var table = new NftTable
|
||||||
|
{
|
||||||
|
Name = "filter",
|
||||||
|
};
|
||||||
|
var map = new NftMap
|
||||||
|
{
|
||||||
|
Name = "bad_map",
|
||||||
|
KeyType = NftMapType.InetService,
|
||||||
|
ValueType = NftMapType.Verdict,
|
||||||
|
};
|
||||||
|
map.Add(NftValue.Interface("eth0"), NftValue.Verdict(NftVerdict.Accept));
|
||||||
|
table.Maps.Add(map);
|
||||||
|
ruleset.Tables.Add(table);
|
||||||
|
|
||||||
|
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(ruleset));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Render_WithNoTables_ThrowsValidationException()
|
public void Render_WithNoTables_ThrowsValidationException()
|
||||||
{
|
{
|
||||||
@@ -130,16 +151,8 @@ public sealed class NftRulesetRendererTests
|
|||||||
KeyType = NftMapType.InetService,
|
KeyType = NftMapType.InetService,
|
||||||
ValueType = NftMapType.Verdict,
|
ValueType = NftMapType.Verdict,
|
||||||
};
|
};
|
||||||
map.Entries.Add(new NftMapEntry
|
map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Accept));
|
||||||
{
|
map.Add(NftValue.Port(443), NftValue.Verdict(NftVerdict.Drop));
|
||||||
Key = NftValue.Port(80),
|
|
||||||
Value = NftValue.Verdict(NftVerdict.Accept),
|
|
||||||
});
|
|
||||||
map.Entries.Add(new NftMapEntry
|
|
||||||
{
|
|
||||||
Key = NftValue.Port(443),
|
|
||||||
Value = NftValue.Verdict(NftVerdict.Drop),
|
|
||||||
});
|
|
||||||
table.Maps.Add(map);
|
table.Maps.Add(map);
|
||||||
|
|
||||||
var chain = new NftChain
|
var chain = new NftChain
|
||||||
|
|||||||
@@ -119,6 +119,61 @@ public sealed class NftablesClientIntegrationTests
|
|||||||
Assert.True(result.IsValid);
|
Assert.True(result.IsValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateAndRenderRuleset_WithTypedMapAndRule_ReturnsValidResult()
|
||||||
|
{
|
||||||
|
if (!CanCreateClient())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = new NftablesClient();
|
||||||
|
var ruleset = new NftRuleset();
|
||||||
|
var table = new NftTable
|
||||||
|
{
|
||||||
|
Family = NftFamily.Inet,
|
||||||
|
Name = "typed_preview",
|
||||||
|
};
|
||||||
|
var set = new NftSet
|
||||||
|
{
|
||||||
|
Name = "blocked_ipv4",
|
||||||
|
Type = NftSetType.Ipv4Address,
|
||||||
|
};
|
||||||
|
set.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.1")));
|
||||||
|
table.Sets.Add(set);
|
||||||
|
|
||||||
|
var map = new NftMap
|
||||||
|
{
|
||||||
|
Name = "service_policy",
|
||||||
|
KeyType = NftMapType.InetService,
|
||||||
|
ValueType = NftMapType.Verdict,
|
||||||
|
};
|
||||||
|
map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Accept));
|
||||||
|
table.Maps.Add(map);
|
||||||
|
|
||||||
|
var chain = new NftChain
|
||||||
|
{
|
||||||
|
Name = "input",
|
||||||
|
Type = NftChainType.Filter,
|
||||||
|
Hook = NftHook.Input,
|
||||||
|
Priority = 0,
|
||||||
|
};
|
||||||
|
chain.Rules.Add(new NftRule
|
||||||
|
{
|
||||||
|
SourceAddressSetName = "blocked_ipv4",
|
||||||
|
TransportProtocol = NftTransportProtocol.Tcp,
|
||||||
|
DestinationPort = NftValue.Port(22),
|
||||||
|
Verdict = NftVerdict.Accept,
|
||||||
|
});
|
||||||
|
table.Chains.Add(chain);
|
||||||
|
ruleset.Tables.Add(table);
|
||||||
|
|
||||||
|
NftRenderedValidationResult result = client.ValidateAndRenderRuleset(ruleset);
|
||||||
|
|
||||||
|
Assert.True(result.ValidationResult.IsValid);
|
||||||
|
Assert.Contains("add map inet typed_preview service_policy", result.RenderedRulesetText, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool CanCreateClient()
|
private static bool CanCreateClient()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -87,6 +87,29 @@ public sealed class NftablesClientUnitTests
|
|||||||
context.LastCommandText);
|
context.LastCommandText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateAndRenderRuleset_ReturnsRenderedTextAndValidation()
|
||||||
|
{
|
||||||
|
var context = new FakeContext
|
||||||
|
{
|
||||||
|
OutputBuffer = "ok",
|
||||||
|
ErrorBuffer = string.Empty,
|
||||||
|
};
|
||||||
|
var client = CreateClient(() => context);
|
||||||
|
|
||||||
|
NftRenderedValidationResult result = client.ValidateAndRenderRuleset(CreateTypedRuleset());
|
||||||
|
|
||||||
|
Assert.True(result.ValidationResult.IsValid);
|
||||||
|
Assert.Equal(result.RenderedRulesetText, context.LastCommandText);
|
||||||
|
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 +
|
||||||
|
"add map inet filter service_policy { type inet_service : verdict; elements = { 80 : accept, 443 : drop }; }" + Environment.NewLine +
|
||||||
|
"add chain inet filter input { type filter hook input priority 0; policy drop; }" + Environment.NewLine +
|
||||||
|
"add rule inet filter input iifname \"eth0\" ip saddr @blocked_ipv4 tcp dport 22 accept" + Environment.NewLine,
|
||||||
|
result.RenderedRulesetText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ApplyRuleset_RendersTypedRulesetAndExecutesCommand()
|
public void ApplyRuleset_RendersTypedRulesetAndExecutesCommand()
|
||||||
{
|
{
|
||||||
@@ -122,6 +145,41 @@ public sealed class NftablesClientUnitTests
|
|||||||
context.LastCommandText);
|
context.LastCommandText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Map_Add_DuplicateKey_ThrowsArgumentException()
|
||||||
|
{
|
||||||
|
var map = new NftMap
|
||||||
|
{
|
||||||
|
Name = "service_policy",
|
||||||
|
KeyType = NftMapType.InetService,
|
||||||
|
ValueType = NftMapType.Verdict,
|
||||||
|
};
|
||||||
|
|
||||||
|
map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Accept));
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentException>(() => map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Drop)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Map_Set_ReplacesValueWithoutChangingInsertionOrder()
|
||||||
|
{
|
||||||
|
var map = new NftMap
|
||||||
|
{
|
||||||
|
Name = "service_policy",
|
||||||
|
KeyType = NftMapType.InetService,
|
||||||
|
ValueType = NftMapType.Verdict,
|
||||||
|
};
|
||||||
|
|
||||||
|
map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Accept));
|
||||||
|
map.Add(NftValue.Port(443), NftValue.Verdict(NftVerdict.Drop));
|
||||||
|
map.Set(NftValue.Port(80), NftValue.Verdict(NftVerdict.Reject));
|
||||||
|
|
||||||
|
Assert.Equal(2, map.Count);
|
||||||
|
Assert.Equal("80", map.Entries[0].Key!.RenderedText);
|
||||||
|
Assert.Equal("reject", map.Entries[0].Value!.RenderedText);
|
||||||
|
Assert.Equal("443", map.Entries[1].Key!.RenderedText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RenderRuleset_ReturnsTypedRulesetTextWithoutExecuting()
|
public void RenderRuleset_ReturnsTypedRulesetTextWithoutExecuting()
|
||||||
{
|
{
|
||||||
@@ -291,16 +349,8 @@ public sealed class NftablesClientUnitTests
|
|||||||
KeyType = NftMapType.InetService,
|
KeyType = NftMapType.InetService,
|
||||||
ValueType = NftMapType.Verdict,
|
ValueType = NftMapType.Verdict,
|
||||||
};
|
};
|
||||||
map.Entries.Add(new NftMapEntry
|
map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Accept));
|
||||||
{
|
map.Add(NftValue.Port(443), NftValue.Verdict(NftVerdict.Drop));
|
||||||
Key = NftValue.Port(80),
|
|
||||||
Value = NftValue.Verdict(NftVerdict.Accept),
|
|
||||||
});
|
|
||||||
map.Entries.Add(new NftMapEntry
|
|
||||||
{
|
|
||||||
Key = NftValue.Port(443),
|
|
||||||
Value = NftValue.Verdict(NftVerdict.Drop),
|
|
||||||
});
|
|
||||||
table.Maps.Add(map);
|
table.Maps.Add(map);
|
||||||
|
|
||||||
var chain = new NftChain
|
var chain = new NftChain
|
||||||
|
|||||||
Reference in New Issue
Block a user