Complete typed map ergonomics and preview API

This commit is contained in:
Vibe Myass
2026-03-16 04:16:40 +00:00
parent e89739a64f
commit 6ae9ccf5e5
9 changed files with 450 additions and 26 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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