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:
|
||||
- typed `NftRuleset` / `NftTable` / `NftSet` / `NftMap` / `NftChain` / `NftRule` authoring
|
||||
- `RenderRuleset`
|
||||
- `ValidateAndRenderRuleset`
|
||||
- `ValidateRuleset`
|
||||
- `ApplyRuleset`
|
||||
- `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.
|
||||
|
||||
## 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
|
||||
|
||||
```csharp
|
||||
@@ -153,7 +203,7 @@ Some operations require elevated privileges or `CAP_NET_ADMIN`, especially when
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -39,6 +39,17 @@ public interface INftablesClient
|
||||
/// <exception cref="NftValidationException">Thrown when the typed model itself is invalid.</exception>
|
||||
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>
|
||||
/// Asynchronously validates a ruleset request using dry-run mode.
|
||||
/// </summary>
|
||||
@@ -61,6 +72,17 @@ public interface INftablesClient
|
||||
/// <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>
|
||||
/// 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>
|
||||
/// Applies a ruleset request.
|
||||
/// </summary>
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace LibNftables;
|
||||
/// </summary>
|
||||
public sealed class NftMap
|
||||
{
|
||||
private readonly List<NftMapEntry> _entries = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map name.
|
||||
/// </summary>
|
||||
@@ -43,7 +45,133 @@ public sealed class NftMap
|
||||
public string? CustomValueTypeExpression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entries declared inline with the map definition.
|
||||
/// Gets the number of entries currently stored in the map.
|
||||
/// </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);
|
||||
}
|
||||
|
||||
string renderedKey = RenderValue(entry.Key!, $"Map '{map.Name}' key");
|
||||
_ = RenderValue(entry.Value!, $"Map '{map.Name}' value");
|
||||
NftValue key = entry.Key ?? throw new NftValidationException($"Map '{map.Name}' contains a null key at index {i}.", 0, null);
|
||||
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))
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
if (chain is null)
|
||||
|
||||
@@ -60,13 +60,23 @@ public sealed class NftablesClient : INftablesClient
|
||||
public NftValidationResult ValidateRuleset(NftRuleset ruleset, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Validate(CreateTypedRequest(ruleset), ct);
|
||||
return ValidateAndRenderRuleset(ruleset, ct).ValidationResult;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RenderRuleset(NftRuleset 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 />
|
||||
public Task<NftValidationResult> ValidateAsync(NftApplyRequest request, CancellationToken ct = default)
|
||||
=> Task.FromResult(Validate(request, ct));
|
||||
@@ -75,6 +85,10 @@ public sealed class NftablesClient : INftablesClient
|
||||
public Task<NftValidationResult> ValidateRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
||||
=> Task.FromResult(ValidateRuleset(ruleset, ct));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<NftRenderedValidationResult> ValidateAndRenderRulesetAsync(NftRuleset ruleset, CancellationToken ct = default)
|
||||
=> Task.FromResult(ValidateAndRenderRuleset(ruleset, ct));
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(NftApplyRequest request, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -29,6 +29,27 @@ public sealed class NftRulesetRendererTests
|
||||
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]
|
||||
public void Render_WithNoTables_ThrowsValidationException()
|
||||
{
|
||||
@@ -130,16 +151,8 @@ public sealed class NftRulesetRendererTests
|
||||
KeyType = NftMapType.InetService,
|
||||
ValueType = NftMapType.Verdict,
|
||||
};
|
||||
map.Entries.Add(new NftMapEntry
|
||||
{
|
||||
Key = NftValue.Port(80),
|
||||
Value = NftValue.Verdict(NftVerdict.Accept),
|
||||
});
|
||||
map.Entries.Add(new NftMapEntry
|
||||
{
|
||||
Key = NftValue.Port(443),
|
||||
Value = NftValue.Verdict(NftVerdict.Drop),
|
||||
});
|
||||
map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Accept));
|
||||
map.Add(NftValue.Port(443), NftValue.Verdict(NftVerdict.Drop));
|
||||
table.Maps.Add(map);
|
||||
|
||||
var chain = new NftChain
|
||||
|
||||
@@ -119,6 +119,61 @@ public sealed class NftablesClientIntegrationTests
|
||||
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()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -87,6 +87,29 @@ public sealed class NftablesClientUnitTests
|
||||
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]
|
||||
public void ApplyRuleset_RendersTypedRulesetAndExecutesCommand()
|
||||
{
|
||||
@@ -122,6 +145,41 @@ public sealed class NftablesClientUnitTests
|
||||
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]
|
||||
public void RenderRuleset_ReturnsTypedRulesetTextWithoutExecuting()
|
||||
{
|
||||
@@ -291,16 +349,8 @@ public sealed class NftablesClientUnitTests
|
||||
KeyType = NftMapType.InetService,
|
||||
ValueType = NftMapType.Verdict,
|
||||
};
|
||||
map.Entries.Add(new NftMapEntry
|
||||
{
|
||||
Key = NftValue.Port(80),
|
||||
Value = NftValue.Verdict(NftVerdict.Accept),
|
||||
});
|
||||
map.Entries.Add(new NftMapEntry
|
||||
{
|
||||
Key = NftValue.Port(443),
|
||||
Value = NftValue.Verdict(NftVerdict.Drop),
|
||||
});
|
||||
map.Add(NftValue.Port(80), NftValue.Verdict(NftVerdict.Accept));
|
||||
map.Add(NftValue.Port(443), NftValue.Verdict(NftVerdict.Drop));
|
||||
table.Maps.Add(map);
|
||||
|
||||
var chain = new NftChain
|
||||
|
||||
Reference in New Issue
Block a user