Expand typed firewall and map API
This commit is contained in:
29
README.md
29
README.md
@@ -7,7 +7,8 @@
|
|||||||
This library is intentionally narrow.
|
This library is intentionally narrow.
|
||||||
|
|
||||||
- High-level managed API:
|
- High-level managed API:
|
||||||
- typed `NftRuleset` / `NftTable` / `NftSet` authoring
|
- typed `NftRuleset` / `NftTable` / `NftSet` / `NftMap` / `NftChain` / `NftRule` authoring
|
||||||
|
- `RenderRuleset`
|
||||||
- `ValidateRuleset`
|
- `ValidateRuleset`
|
||||||
- `ApplyRuleset`
|
- `ApplyRuleset`
|
||||||
- `Snapshot`
|
- `Snapshot`
|
||||||
@@ -17,7 +18,7 @@ This library is intentionally narrow.
|
|||||||
|
|
||||||
Non-goals for the current release:
|
Non-goals for the current release:
|
||||||
|
|
||||||
- Typed rule expressions, maps, and snapshot parsing back into object models
|
- Full nft expression parity 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
|
||||||
|
|
||||||
@@ -79,11 +80,31 @@ var blocked = new NftSet
|
|||||||
Name = "blocked_ipv4",
|
Name = "blocked_ipv4",
|
||||||
Type = NftSetType.Ipv4Address,
|
Type = NftSetType.Ipv4Address,
|
||||||
};
|
};
|
||||||
blocked.Elements.Add("10.0.0.1");
|
blocked.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.1")));
|
||||||
blocked.Elements.Add("10.0.0.2");
|
blocked.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.2")));
|
||||||
table.Sets.Add(blocked);
|
table.Sets.Add(blocked);
|
||||||
|
|
||||||
|
var chain = new NftChain
|
||||||
|
{
|
||||||
|
Name = "input",
|
||||||
|
Type = NftChainType.Filter,
|
||||||
|
Hook = NftHook.Input,
|
||||||
|
Priority = 0,
|
||||||
|
Policy = NftChainPolicy.Drop,
|
||||||
|
};
|
||||||
|
chain.Rules.Add(new NftRule
|
||||||
|
{
|
||||||
|
InputInterface = NftValue.Interface("eth0"),
|
||||||
|
SourceAddressSetName = "blocked_ipv4",
|
||||||
|
TransportProtocol = NftTransportProtocol.Tcp,
|
||||||
|
DestinationPort = NftValue.Port(22),
|
||||||
|
Verdict = NftVerdict.Accept,
|
||||||
|
});
|
||||||
|
table.Chains.Add(chain);
|
||||||
|
|
||||||
ruleset.Tables.Add(table);
|
ruleset.Tables.Add(table);
|
||||||
|
|
||||||
|
string preview = client.RenderRuleset(ruleset);
|
||||||
var validation = client.ValidateRuleset(ruleset);
|
var validation = client.ValidateRuleset(ruleset);
|
||||||
if (validation.IsValid)
|
if (validation.IsValid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ 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 ValidateRuleset(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
NftValidationResult ValidateRuleset(NftRuleset ruleset, System.Threading.CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a typed nftables ruleset to nft command text without executing it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ruleset">The typed ruleset to render.</param>
|
||||||
|
/// <returns>The rendered nft command text.</returns>
|
||||||
|
/// <exception cref="NftValidationException">Thrown when the typed model itself is invalid.</exception>
|
||||||
|
string RenderRuleset(NftRuleset ruleset);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously validates a ruleset request using dry-run mode.
|
/// Asynchronously validates a ruleset request using dry-run mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
43
src/LibNftables/NftChain.cs
Normal file
43
src/LibNftables/NftChain.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a typed nftables chain definition.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NftChain
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the chain name.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the chain type for base chains.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Leave <see langword="null"/> to model a regular chain instead of a base chain.
|
||||||
|
/// </remarks>
|
||||||
|
public NftChainType? Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the hook for base chains.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Leave <see langword="null"/> to model a regular chain instead of a base chain.
|
||||||
|
/// </remarks>
|
||||||
|
public NftHook? Hook { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the priority for base chains.
|
||||||
|
/// </summary>
|
||||||
|
public int? Priority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the default policy for base chains.
|
||||||
|
/// </summary>
|
||||||
|
public NftChainPolicy? Policy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the rules declared in this chain.
|
||||||
|
/// </summary>
|
||||||
|
public IList<NftRule> Rules { get; } = new List<NftRule>();
|
||||||
|
}
|
||||||
17
src/LibNftables/NftChainPolicy.cs
Normal file
17
src/LibNftables/NftChainPolicy.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported policies for typed base chains.
|
||||||
|
/// </summary>
|
||||||
|
public enum NftChainPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Accept packets by default.
|
||||||
|
/// </summary>
|
||||||
|
Accept,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drop packets by default.
|
||||||
|
/// </summary>
|
||||||
|
Drop,
|
||||||
|
}
|
||||||
22
src/LibNftables/NftChainType.cs
Normal file
22
src/LibNftables/NftChainType.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported nftables chain types for the typed high-level model.
|
||||||
|
/// </summary>
|
||||||
|
public enum NftChainType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filter chain.
|
||||||
|
/// </summary>
|
||||||
|
Filter,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// NAT chain.
|
||||||
|
/// </summary>
|
||||||
|
Nat,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Route chain.
|
||||||
|
/// </summary>
|
||||||
|
Route,
|
||||||
|
}
|
||||||
37
src/LibNftables/NftHook.cs
Normal file
37
src/LibNftables/NftHook.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported nftables base-chain hooks for the typed high-level model.
|
||||||
|
/// </summary>
|
||||||
|
public enum NftHook
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Input hook.
|
||||||
|
/// </summary>
|
||||||
|
Input,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forward hook.
|
||||||
|
/// </summary>
|
||||||
|
Forward,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output hook.
|
||||||
|
/// </summary>
|
||||||
|
Output,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prerouting hook.
|
||||||
|
/// </summary>
|
||||||
|
Prerouting,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Postrouting hook.
|
||||||
|
/// </summary>
|
||||||
|
Postrouting,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ingress hook.
|
||||||
|
/// </summary>
|
||||||
|
Ingress,
|
||||||
|
}
|
||||||
49
src/LibNftables/NftMap.cs
Normal file
49
src/LibNftables/NftMap.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a typed nftables map definition backed by native C# collections.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NftMap
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the map name.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a common typed key type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Set either <see cref="KeyType"/> or <see cref="CustomKeyTypeExpression"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public NftMapType? KeyType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a custom nftables key-type expression for the map.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Set either <see cref="KeyType"/> or <see cref="CustomKeyTypeExpression"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public string? CustomKeyTypeExpression { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a common typed value type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Set either <see cref="ValueType"/> or <see cref="CustomValueTypeExpression"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public NftMapType? ValueType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a custom nftables value-type expression for the map.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Set either <see cref="ValueType"/> or <see cref="CustomValueTypeExpression"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public string? CustomValueTypeExpression { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the entries declared inline with the map definition.
|
||||||
|
/// </summary>
|
||||||
|
public IList<NftMapEntry> Entries { get; } = new List<NftMapEntry>();
|
||||||
|
}
|
||||||
17
src/LibNftables/NftMapEntry.cs
Normal file
17
src/LibNftables/NftMapEntry.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a key/value entry in a typed nftables map.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NftMapEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the entry key.
|
||||||
|
/// </summary>
|
||||||
|
public NftValue? Key { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the entry value.
|
||||||
|
/// </summary>
|
||||||
|
public NftValue? Value { get; set; }
|
||||||
|
}
|
||||||
42
src/LibNftables/NftMapType.cs
Normal file
42
src/LibNftables/NftMapType.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported typed key/value data kinds for nftables maps.
|
||||||
|
/// </summary>
|
||||||
|
public enum NftMapType
|
||||||
|
{
|
||||||
|
/// <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,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verdict values (<c>verdict</c>).
|
||||||
|
/// </summary>
|
||||||
|
Verdict,
|
||||||
|
}
|
||||||
72
src/LibNftables/NftRule.cs
Normal file
72
src/LibNftables/NftRule.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a typed nftables firewall rule in the supported high-level subset.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NftRule
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a source-address value match.
|
||||||
|
/// </summary>
|
||||||
|
public NftValue? SourceAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of a set to use for source-address membership.
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceAddressSetName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a destination-address value match.
|
||||||
|
/// </summary>
|
||||||
|
public NftValue? DestinationAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of a set to use for destination-address membership.
|
||||||
|
/// </summary>
|
||||||
|
public string? DestinationAddressSetName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the transport protocol used for port matches.
|
||||||
|
/// </summary>
|
||||||
|
public NftTransportProtocol? TransportProtocol { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a source-port value match.
|
||||||
|
/// </summary>
|
||||||
|
public NftValue? SourcePort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of a set to use for source-port membership.
|
||||||
|
/// </summary>
|
||||||
|
public string? SourcePortSetName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a destination-port value match.
|
||||||
|
/// </summary>
|
||||||
|
public NftValue? DestinationPort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of a set to use for destination-port membership.
|
||||||
|
/// </summary>
|
||||||
|
public string? DestinationPortSetName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an input-interface match.
|
||||||
|
/// </summary>
|
||||||
|
public NftValue? InputInterface { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an output-interface match.
|
||||||
|
/// </summary>
|
||||||
|
public NftValue? OutputInterface { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the verdict action.
|
||||||
|
/// </summary>
|
||||||
|
public NftVerdict Verdict { get; set; } = NftVerdict.Accept;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the jump target chain name when <see cref="Verdict"/> is <see cref="NftVerdict.Jump"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string? JumpTarget { get; set; }
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace LibNftables;
|
namespace LibNftables;
|
||||||
@@ -32,60 +33,302 @@ internal static class NftRulesetRenderer
|
|||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Append("add table ")
|
RenderTable(builder, 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();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void RenderTable(StringBuilder builder, NftTable table)
|
||||||
|
{
|
||||||
|
string family = ToKeyword(table.Family);
|
||||||
|
|
||||||
|
builder.Append("add table ")
|
||||||
|
.Append(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderSet(builder, table, set, family);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapNames = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (NftMap map in table.Maps)
|
||||||
|
{
|
||||||
|
ValidateMap(map, table);
|
||||||
|
|
||||||
|
if (!mapNames.Add(map.Name!))
|
||||||
|
{
|
||||||
|
throw new NftValidationException($"Table '{table.Name}' contains duplicate map '{map.Name}'.", 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderMap(builder, table, map, family);
|
||||||
|
}
|
||||||
|
|
||||||
|
var chainsByName = new Dictionary<string, NftChain>(StringComparer.Ordinal);
|
||||||
|
foreach (NftChain chain in table.Chains)
|
||||||
|
{
|
||||||
|
ValidateChain(chain, table);
|
||||||
|
|
||||||
|
if (!chainsByName.TryAdd(chain.Name!, chain))
|
||||||
|
{
|
||||||
|
throw new NftValidationException($"Table '{table.Name}' contains duplicate chain '{chain.Name}'.", 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderChain(builder, table, chain, family);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (NftChain chain in table.Chains)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < chain.Rules.Count; i++)
|
||||||
|
{
|
||||||
|
RenderRule(builder, table, chain, chain.Rules[i], i, family, chainsByName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RenderSet(StringBuilder builder, NftTable table, NftSet set, string family)
|
||||||
|
{
|
||||||
|
builder.Append("add set ")
|
||||||
|
.Append(family)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(table.Name)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(set.Name)
|
||||||
|
.Append(" { type ")
|
||||||
|
.Append(GetSetTypeExpression(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(RenderValue(set.Elements[i], $"Set '{set.Name}' element"));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(" };");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(" }")
|
||||||
|
.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RenderMap(StringBuilder builder, NftTable table, NftMap map, string family)
|
||||||
|
{
|
||||||
|
builder.Append("add map ")
|
||||||
|
.Append(family)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(table.Name)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(map.Name)
|
||||||
|
.Append(" { type ")
|
||||||
|
.Append(GetMapTypeExpression(map, isKey: true))
|
||||||
|
.Append(" : ")
|
||||||
|
.Append(GetMapTypeExpression(map, isKey: false))
|
||||||
|
.Append(';');
|
||||||
|
|
||||||
|
if (map.Entries.Count > 0)
|
||||||
|
{
|
||||||
|
builder.Append(" elements = { ");
|
||||||
|
|
||||||
|
for (int i = 0; i < map.Entries.Count; i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
builder.Append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
NftMapEntry entry = map.Entries[i];
|
||||||
|
builder.Append(RenderValue(entry.Key!, $"Map '{map.Name}' key"))
|
||||||
|
.Append(" : ")
|
||||||
|
.Append(RenderValue(entry.Value!, $"Map '{map.Name}' value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(" };");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(" }")
|
||||||
|
.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RenderChain(StringBuilder builder, NftTable table, NftChain chain, string family)
|
||||||
|
{
|
||||||
|
builder.Append("add chain ")
|
||||||
|
.Append(family)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(table.Name)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(chain.Name);
|
||||||
|
|
||||||
|
if (IsBaseChain(chain))
|
||||||
|
{
|
||||||
|
builder.Append(" { type ")
|
||||||
|
.Append(ToKeyword(chain.Type!.Value))
|
||||||
|
.Append(" hook ")
|
||||||
|
.Append(ToKeyword(chain.Hook!.Value))
|
||||||
|
.Append(" priority ")
|
||||||
|
.Append(chain.Priority!.Value)
|
||||||
|
.Append(';');
|
||||||
|
|
||||||
|
if (chain.Policy.HasValue)
|
||||||
|
{
|
||||||
|
builder.Append(" policy ")
|
||||||
|
.Append(ToKeyword(chain.Policy.Value))
|
||||||
|
.Append(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(" }");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RenderRule(
|
||||||
|
StringBuilder builder,
|
||||||
|
NftTable table,
|
||||||
|
NftChain chain,
|
||||||
|
NftRule rule,
|
||||||
|
int index,
|
||||||
|
string family,
|
||||||
|
IReadOnlyDictionary<string, NftChain> chainsByName)
|
||||||
|
{
|
||||||
|
ValidateRule(rule, table, chain, index, chainsByName);
|
||||||
|
|
||||||
|
var tokens = new List<string>();
|
||||||
|
|
||||||
|
if (rule.InputInterface is not null)
|
||||||
|
{
|
||||||
|
tokens.Add("iifname " + RenderTypedField(rule.InputInterface, NftValueKind.Interface, "InputInterface"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.OutputInterface is not null)
|
||||||
|
{
|
||||||
|
tokens.Add("oifname " + RenderTypedField(rule.OutputInterface, NftValueKind.Interface, "OutputInterface"));
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendAddressMatch(tokens, rule.SourceAddress, rule.SourceAddressSetName, table, "saddr");
|
||||||
|
AppendAddressMatch(tokens, rule.DestinationAddress, rule.DestinationAddressSetName, table, "daddr");
|
||||||
|
|
||||||
|
bool hasPortMatch = rule.SourcePort is not null ||
|
||||||
|
rule.DestinationPort is not null ||
|
||||||
|
!string.IsNullOrWhiteSpace(rule.SourcePortSetName) ||
|
||||||
|
!string.IsNullOrWhiteSpace(rule.DestinationPortSetName);
|
||||||
|
|
||||||
|
if (hasPortMatch)
|
||||||
|
{
|
||||||
|
tokens.Add(ToKeyword(rule.TransportProtocol!.Value));
|
||||||
|
AppendPortMatch(tokens, rule.SourcePort, rule.SourcePortSetName, table, "sport");
|
||||||
|
AppendPortMatch(tokens, rule.DestinationPort, rule.DestinationPortSetName, table, "dport");
|
||||||
|
}
|
||||||
|
else if (rule.TransportProtocol.HasValue)
|
||||||
|
{
|
||||||
|
tokens.Add("meta l4proto " + ToKeyword(rule.TransportProtocol.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.Add(RenderVerdict(rule));
|
||||||
|
|
||||||
|
builder.Append("add rule ")
|
||||||
|
.Append(family)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(table.Name)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(chain.Name)
|
||||||
|
.Append(' ')
|
||||||
|
.Append(string.Join(" ", tokens))
|
||||||
|
.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendAddressMatch(List<string> tokens, NftValue? value, string? setName, NftTable table, string direction)
|
||||||
|
{
|
||||||
|
if (value is null && string.IsNullOrWhiteSpace(setName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is not null)
|
||||||
|
{
|
||||||
|
ValidateValueKind(value, "address match", NftValueKind.Address, NftValueKind.Network);
|
||||||
|
string prefix = value.AddressFamily switch
|
||||||
|
{
|
||||||
|
AddressFamily.InterNetwork => "ip",
|
||||||
|
AddressFamily.InterNetworkV6 => "ip6",
|
||||||
|
_ => throw new NftValidationException("Address match must be IPv4 or IPv6.", 0, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
tokens.Add(prefix + " " + direction + " " + RenderValue(value, "address match"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string resolvedSetName = setName!;
|
||||||
|
NftSet set = FindSet(table, resolvedSetName);
|
||||||
|
string prefixFromSet = GetAddressQualifierForSet(set);
|
||||||
|
tokens.Add(prefixFromSet + " " + direction + " @" + resolvedSetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendPortMatch(List<string> tokens, NftValue? value, string? setName, NftTable table, string direction)
|
||||||
|
{
|
||||||
|
if (value is null && string.IsNullOrWhiteSpace(setName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is not null)
|
||||||
|
{
|
||||||
|
ValidateValueKind(value, "port match", NftValueKind.Port, NftValueKind.Raw);
|
||||||
|
tokens.Add(direction + " " + RenderValue(value, "port match"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string resolvedSetName = setName!;
|
||||||
|
NftSet set = FindSet(table, resolvedSetName);
|
||||||
|
EnsureSetType(set, NftSetType.InetService, $"Set '{set.Name}' must use inet_service to be referenced as a port set.");
|
||||||
|
tokens.Add(direction + " @" + resolvedSetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RenderVerdict(NftRule rule)
|
||||||
|
=> rule.Verdict switch
|
||||||
|
{
|
||||||
|
NftVerdict.Accept => "accept",
|
||||||
|
NftVerdict.Drop => "drop",
|
||||||
|
NftVerdict.Reject => "reject",
|
||||||
|
NftVerdict.Jump => "jump " + rule.JumpTarget,
|
||||||
|
_ => throw new NftValidationException($"Unsupported verdict '{rule.Verdict}'.", 0, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string RenderTypedField(NftValue value, NftValueKind expectedKind, string fieldName)
|
||||||
|
{
|
||||||
|
ValidateValueKind(value, fieldName, expectedKind, NftValueKind.Raw);
|
||||||
|
return RenderValue(value, fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RenderValue(NftValue value, string fieldName)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
throw new NftValidationException($"{fieldName} must not be null.", 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateToken(value.RenderedText, fieldName);
|
||||||
|
return value.RenderedText;
|
||||||
|
}
|
||||||
|
|
||||||
private static void ValidateTable(NftTable table)
|
private static void ValidateTable(NftTable table)
|
||||||
{
|
{
|
||||||
if (table is null)
|
if (table is null)
|
||||||
@@ -100,42 +343,236 @@ internal static class NftRulesetRenderer
|
|||||||
{
|
{
|
||||||
if (set is null)
|
if (set is null)
|
||||||
{
|
{
|
||||||
throw new NftValidationException(
|
throw new NftValidationException($"Table '{table.Name}' contains a null set definition.", 0, null);
|
||||||
$"Table '{table.Name}' contains a null set definition.",
|
|
||||||
0,
|
|
||||||
null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateIdentifier(set.Name, "Set name");
|
ValidateIdentifier(set.Name, "Set name");
|
||||||
|
ValidateExclusiveTypeSources(
|
||||||
|
set.Type.HasValue,
|
||||||
|
!string.IsNullOrWhiteSpace(set.CustomTypeExpression),
|
||||||
|
$"Set '{set.Name}' must specify exactly one type source: Type or CustomTypeExpression.");
|
||||||
|
|
||||||
bool hasTypedType = set.Type.HasValue;
|
if (!string.IsNullOrWhiteSpace(set.CustomTypeExpression))
|
||||||
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");
|
ValidateToken(set.CustomTypeExpression!, $"Set '{set.Name}' custom type");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < set.Elements.Count; i++)
|
for (int i = 0; i < set.Elements.Count; i++)
|
||||||
{
|
{
|
||||||
string? element = set.Elements[i];
|
if (set.Elements[i] is null)
|
||||||
if (string.IsNullOrWhiteSpace(element))
|
{
|
||||||
|
throw new NftValidationException($"Set '{set.Name}' contains a null element at index {i}.", 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = RenderValue(set.Elements[i], $"Set '{set.Name}' element");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateMap(NftMap map, NftTable table)
|
||||||
|
{
|
||||||
|
if (map is null)
|
||||||
|
{
|
||||||
|
throw new NftValidationException($"Table '{table.Name}' contains a null map definition.", 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateIdentifier(map.Name, "Map name");
|
||||||
|
ValidateExclusiveTypeSources(
|
||||||
|
map.KeyType.HasValue,
|
||||||
|
!string.IsNullOrWhiteSpace(map.CustomKeyTypeExpression),
|
||||||
|
$"Map '{map.Name}' must specify exactly one key type source: KeyType or CustomKeyTypeExpression.");
|
||||||
|
ValidateExclusiveTypeSources(
|
||||||
|
map.ValueType.HasValue,
|
||||||
|
!string.IsNullOrWhiteSpace(map.CustomValueTypeExpression),
|
||||||
|
$"Map '{map.Name}' must specify exactly one value type source: ValueType or CustomValueTypeExpression.");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(map.CustomKeyTypeExpression))
|
||||||
|
{
|
||||||
|
ValidateToken(map.CustomKeyTypeExpression!, $"Map '{map.Name}' custom key type");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(map.CustomValueTypeExpression))
|
||||||
|
{
|
||||||
|
ValidateToken(map.CustomValueTypeExpression!, $"Map '{map.Name}' custom value type");
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
for (int i = 0; i < map.Entries.Count; i++)
|
||||||
|
{
|
||||||
|
NftMapEntry? entry = map.Entries[i];
|
||||||
|
if (entry is 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");
|
||||||
|
_ = RenderValue(entry.Value!, $"Map '{map.Name}' value");
|
||||||
|
|
||||||
|
if (!keys.Add(renderedKey))
|
||||||
|
{
|
||||||
|
throw new NftValidationException($"Map '{map.Name}' contains duplicate key '{renderedKey}'.", 0, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateChain(NftChain chain, NftTable table)
|
||||||
|
{
|
||||||
|
if (chain is null)
|
||||||
|
{
|
||||||
|
throw new NftValidationException($"Table '{table.Name}' contains a null chain definition.", 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateIdentifier(chain.Name, "Chain name");
|
||||||
|
|
||||||
|
bool hasBaseType = chain.Type.HasValue;
|
||||||
|
bool hasHook = chain.Hook.HasValue;
|
||||||
|
bool hasPriority = chain.Priority.HasValue;
|
||||||
|
bool hasAnyBaseConfig = hasBaseType || hasHook || hasPriority || chain.Policy.HasValue;
|
||||||
|
|
||||||
|
if (hasAnyBaseConfig && !(hasBaseType && hasHook && hasPriority))
|
||||||
|
{
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Chain '{chain.Name}' must set Type, Hook, and Priority together when configured as a base chain.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAnyBaseConfig && chain.Policy.HasValue)
|
||||||
|
{
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Chain '{chain.Name}' policy is only valid on base chains.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateRule(
|
||||||
|
NftRule rule,
|
||||||
|
NftTable table,
|
||||||
|
NftChain chain,
|
||||||
|
int index,
|
||||||
|
IReadOnlyDictionary<string, NftChain> chainsByName)
|
||||||
|
{
|
||||||
|
if (rule is null)
|
||||||
|
{
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Chain '{chain.Name}' contains a null rule at index {index}.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateExclusiveRuleCriterion(rule.SourceAddress, rule.SourceAddressSetName, "source address");
|
||||||
|
ValidateExclusiveRuleCriterion(rule.DestinationAddress, rule.DestinationAddressSetName, "destination address");
|
||||||
|
ValidateExclusiveRuleCriterion(rule.SourcePort, rule.SourcePortSetName, "source port");
|
||||||
|
ValidateExclusiveRuleCriterion(rule.DestinationPort, rule.DestinationPortSetName, "destination port");
|
||||||
|
|
||||||
|
if (rule.InputInterface is not null)
|
||||||
|
{
|
||||||
|
ValidateValueKind(rule.InputInterface, "InputInterface", NftValueKind.Interface, NftValueKind.Raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.OutputInterface is not null)
|
||||||
|
{
|
||||||
|
ValidateValueKind(rule.OutputInterface, "OutputInterface", NftValueKind.Interface, NftValueKind.Raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.SourceAddress is not null)
|
||||||
|
{
|
||||||
|
ValidateValueKind(rule.SourceAddress, "SourceAddress", NftValueKind.Address, NftValueKind.Network);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.DestinationAddress is not null)
|
||||||
|
{
|
||||||
|
ValidateValueKind(rule.DestinationAddress, "DestinationAddress", NftValueKind.Address, NftValueKind.Network);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.SourcePort is not null)
|
||||||
|
{
|
||||||
|
ValidateValueKind(rule.SourcePort, "SourcePort", NftValueKind.Port, NftValueKind.Raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.DestinationPort is not null)
|
||||||
|
{
|
||||||
|
ValidateValueKind(rule.DestinationPort, "DestinationPort", NftValueKind.Port, NftValueKind.Raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasPortMatch = rule.SourcePort is not null ||
|
||||||
|
rule.DestinationPort is not null ||
|
||||||
|
!string.IsNullOrWhiteSpace(rule.SourcePortSetName) ||
|
||||||
|
!string.IsNullOrWhiteSpace(rule.DestinationPortSetName);
|
||||||
|
|
||||||
|
if (hasPortMatch && !rule.TransportProtocol.HasValue)
|
||||||
|
{
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Chain '{chain.Name}' rule at index {index} must specify TransportProtocol when matching ports.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateReferencedSet(table, rule.SourceAddressSetName, index, chain.Name!, NftSetType.Ipv4Address, NftSetType.Ipv6Address);
|
||||||
|
ValidateReferencedSet(table, rule.DestinationAddressSetName, index, chain.Name!, NftSetType.Ipv4Address, NftSetType.Ipv6Address);
|
||||||
|
ValidateReferencedSet(table, rule.SourcePortSetName, index, chain.Name!, NftSetType.InetService);
|
||||||
|
ValidateReferencedSet(table, rule.DestinationPortSetName, index, chain.Name!, NftSetType.InetService);
|
||||||
|
|
||||||
|
if (rule.Verdict == NftVerdict.Jump)
|
||||||
|
{
|
||||||
|
ValidateIdentifier(rule.JumpTarget, "Jump target");
|
||||||
|
if (!chainsByName.ContainsKey(rule.JumpTarget!))
|
||||||
{
|
{
|
||||||
throw new NftValidationException(
|
throw new NftValidationException(
|
||||||
$"Set '{set.Name}' contains an empty element at index {i}.",
|
$"Chain '{chain.Name}' rule at index {index} jumps to unknown chain '{rule.JumpTarget}'.",
|
||||||
0,
|
0,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(rule.JumpTarget))
|
||||||
|
{
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Chain '{chain.Name}' rule at index {index} cannot set JumpTarget unless Verdict is Jump.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ValidateToken(element, $"Set '{set.Name}' element");
|
private static void ValidateExclusiveTypeSources(bool hasTypedType, bool hasCustomType, string message)
|
||||||
|
{
|
||||||
|
if (hasTypedType == hasCustomType)
|
||||||
|
{
|
||||||
|
throw new NftValidationException(message, 0, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateExclusiveRuleCriterion(NftValue? value, string? setName, string fieldName)
|
||||||
|
{
|
||||||
|
bool hasValue = value is not null;
|
||||||
|
bool hasSet = !string.IsNullOrWhiteSpace(setName);
|
||||||
|
if (hasValue && hasSet)
|
||||||
|
{
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Rule must not specify both a direct value and a set reference for {fieldName}.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateReferencedSet(
|
||||||
|
NftTable table,
|
||||||
|
string? setName,
|
||||||
|
int index,
|
||||||
|
string chainName,
|
||||||
|
params NftSetType[] allowedTypes)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(setName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NftSet set = FindSet(table, setName);
|
||||||
|
if (!set.Type.HasValue || Array.IndexOf(allowedTypes, set.Type.Value) < 0)
|
||||||
|
{
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"Chain '{chainName}' rule at index {index} references set '{setName}' with an unsupported type for this match.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,10 +587,7 @@ internal static class NftRulesetRenderer
|
|||||||
{
|
{
|
||||||
if (char.IsWhiteSpace(ch) || ch is '{' or '}' or ';' or '"' or '\'')
|
if (char.IsWhiteSpace(ch) || ch is '{' or '}' or ';' or '"' or '\'')
|
||||||
{
|
{
|
||||||
throw new NftValidationException(
|
throw new NftValidationException($"{fieldName} contains unsupported character '{ch}'.", 0, null);
|
||||||
$"{fieldName} contains unsupported character '{ch}'.",
|
|
||||||
0,
|
|
||||||
null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,15 +598,63 @@ internal static class NftRulesetRenderer
|
|||||||
{
|
{
|
||||||
if (ch is '\r' or '\n' or ';')
|
if (ch is '\r' or '\n' or ';')
|
||||||
{
|
{
|
||||||
throw new NftValidationException(
|
throw new NftValidationException($"{fieldName} contains unsupported character '{ch}'.", 0, null);
|
||||||
$"{fieldName} contains unsupported character '{ch}'.",
|
|
||||||
0,
|
|
||||||
null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetTypeExpression(NftSet set)
|
private static void ValidateValueKind(NftValue value, string fieldName, params NftValueKind[] allowedKinds)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
throw new NftValidationException($"{fieldName} must not be null.", 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.IndexOf(allowedKinds, value.Kind) >= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NftValidationException(
|
||||||
|
$"{fieldName} does not support value kind '{value.Kind}'.",
|
||||||
|
0,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NftSet FindSet(NftTable table, string setName)
|
||||||
|
{
|
||||||
|
NftSet? set = table.Sets.FirstOrDefault(x => string.Equals(x.Name, setName, StringComparison.Ordinal));
|
||||||
|
if (set is null)
|
||||||
|
{
|
||||||
|
throw new NftValidationException($"Table '{table.Name}' does not contain a set named '{setName}'.", 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureSetType(NftSet set, NftSetType expectedType, string message)
|
||||||
|
{
|
||||||
|
if (set.Type != expectedType)
|
||||||
|
{
|
||||||
|
throw new NftValidationException(message, 0, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAddressQualifierForSet(NftSet set)
|
||||||
|
=> set.Type switch
|
||||||
|
{
|
||||||
|
NftSetType.Ipv4Address => "ip",
|
||||||
|
NftSetType.Ipv6Address => "ip6",
|
||||||
|
_ => throw new NftValidationException(
|
||||||
|
$"Set '{set.Name}' cannot be used as an address set because its type is not an IP address type.",
|
||||||
|
0,
|
||||||
|
null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool IsBaseChain(NftChain chain)
|
||||||
|
=> chain.Type.HasValue || chain.Hook.HasValue || chain.Priority.HasValue || chain.Policy.HasValue;
|
||||||
|
|
||||||
|
private static string GetSetTypeExpression(NftSet set)
|
||||||
=> set.Type switch
|
=> set.Type switch
|
||||||
{
|
{
|
||||||
NftSetType.Ipv4Address => "ipv4_addr",
|
NftSetType.Ipv4Address => "ipv4_addr",
|
||||||
@@ -184,6 +666,24 @@ internal static class NftRulesetRenderer
|
|||||||
_ => set.CustomTypeExpression!,
|
_ => set.CustomTypeExpression!,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string GetMapTypeExpression(NftMap map, bool isKey)
|
||||||
|
{
|
||||||
|
NftMapType? typedType = isKey ? map.KeyType : map.ValueType;
|
||||||
|
string? customType = isKey ? map.CustomKeyTypeExpression : map.CustomValueTypeExpression;
|
||||||
|
|
||||||
|
return typedType switch
|
||||||
|
{
|
||||||
|
NftMapType.Ipv4Address => "ipv4_addr",
|
||||||
|
NftMapType.Ipv6Address => "ipv6_addr",
|
||||||
|
NftMapType.InetService => "inet_service",
|
||||||
|
NftMapType.EtherAddress => "ether_addr",
|
||||||
|
NftMapType.InterfaceName => "ifname",
|
||||||
|
NftMapType.Mark => "mark",
|
||||||
|
NftMapType.Verdict => "verdict",
|
||||||
|
_ => customType!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string ToKeyword(NftFamily family)
|
private static string ToKeyword(NftFamily family)
|
||||||
=> family switch
|
=> family switch
|
||||||
{
|
{
|
||||||
@@ -195,4 +695,41 @@ internal static class NftRulesetRenderer
|
|||||||
NftFamily.Netdev => "netdev",
|
NftFamily.Netdev => "netdev",
|
||||||
_ => throw new NftValidationException($"Unsupported nft family '{family}'.", 0, null),
|
_ => throw new NftValidationException($"Unsupported nft family '{family}'.", 0, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string ToKeyword(NftChainType chainType)
|
||||||
|
=> chainType switch
|
||||||
|
{
|
||||||
|
NftChainType.Filter => "filter",
|
||||||
|
NftChainType.Nat => "nat",
|
||||||
|
NftChainType.Route => "route",
|
||||||
|
_ => throw new NftValidationException($"Unsupported chain type '{chainType}'.", 0, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ToKeyword(NftHook hook)
|
||||||
|
=> hook switch
|
||||||
|
{
|
||||||
|
NftHook.Input => "input",
|
||||||
|
NftHook.Forward => "forward",
|
||||||
|
NftHook.Output => "output",
|
||||||
|
NftHook.Prerouting => "prerouting",
|
||||||
|
NftHook.Postrouting => "postrouting",
|
||||||
|
NftHook.Ingress => "ingress",
|
||||||
|
_ => throw new NftValidationException($"Unsupported hook '{hook}'.", 0, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ToKeyword(NftChainPolicy policy)
|
||||||
|
=> policy switch
|
||||||
|
{
|
||||||
|
NftChainPolicy.Accept => "accept",
|
||||||
|
NftChainPolicy.Drop => "drop",
|
||||||
|
_ => throw new NftValidationException($"Unsupported chain policy '{policy}'.", 0, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ToKeyword(NftTransportProtocol protocol)
|
||||||
|
=> protocol switch
|
||||||
|
{
|
||||||
|
NftTransportProtocol.Tcp => "tcp",
|
||||||
|
NftTransportProtocol.Udp => "udp",
|
||||||
|
_ => throw new NftValidationException($"Unsupported transport protocol '{protocol}'.", 0, null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public sealed class NftSet
|
|||||||
/// Gets the set elements to declare inline with the set definition.
|
/// Gets the set elements to declare inline with the set definition.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Values are rendered as nftables literals and should already be valid for the selected type.
|
/// Values are strongly typed where possible and rendered to nftables literals by the high-level renderer.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<string> Elements { get; } = new List<string>();
|
public IList<NftValue> Elements { get; } = new List<NftValue>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,14 @@ public sealed class NftTable
|
|||||||
/// Gets the sets declared in this table.
|
/// Gets the sets declared in this table.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<NftSet> Sets { get; } = new List<NftSet>();
|
public IList<NftSet> Sets { get; } = new List<NftSet>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maps declared in this table.
|
||||||
|
/// </summary>
|
||||||
|
public IList<NftMap> Maps { get; } = new List<NftMap>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the chains declared in this table.
|
||||||
|
/// </summary>
|
||||||
|
public IList<NftChain> Chains { get; } = new List<NftChain>();
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/LibNftables/NftTransportProtocol.cs
Normal file
17
src/LibNftables/NftTransportProtocol.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported L4 transport protocols for the typed rule model.
|
||||||
|
/// </summary>
|
||||||
|
public enum NftTransportProtocol
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// TCP.
|
||||||
|
/// </summary>
|
||||||
|
Tcp,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UDP.
|
||||||
|
/// </summary>
|
||||||
|
Udp,
|
||||||
|
}
|
||||||
127
src/LibNftables/NftValue.cs
Normal file
127
src/LibNftables/NftValue.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a strongly typed value that can be rendered into nftables literal syntax.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NftValue
|
||||||
|
{
|
||||||
|
private NftValue(NftValueKind kind, string renderedText, AddressFamily? addressFamily = null)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
RenderedText = renderedText;
|
||||||
|
AddressFamily = addressFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value kind.
|
||||||
|
/// </summary>
|
||||||
|
public NftValueKind Kind { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the rendered nftables literal text.
|
||||||
|
/// </summary>
|
||||||
|
public string RenderedText { get; }
|
||||||
|
|
||||||
|
internal AddressFamily? AddressFamily { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a raw nft literal token.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The literal token text.</param>
|
||||||
|
/// <returns>A typed value that renders exactly as the provided token.</returns>
|
||||||
|
public static NftValue Raw(string token)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(token);
|
||||||
|
return new NftValue(NftValueKind.Raw, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a single IP address value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The IP address.</param>
|
||||||
|
/// <returns>A typed value for the address.</returns>
|
||||||
|
public static NftValue Address(IPAddress address)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(address);
|
||||||
|
return new NftValue(NftValueKind.Address, address.ToString(), address.AddressFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an IP network/CIDR value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAddress">The network base address.</param>
|
||||||
|
/// <param name="prefixLength">The network prefix length.</param>
|
||||||
|
/// <returns>A typed value for the network.</returns>
|
||||||
|
public static NftValue Network(IPAddress baseAddress, int prefixLength)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(baseAddress);
|
||||||
|
|
||||||
|
int maxPrefix = baseAddress.AddressFamily switch
|
||||||
|
{
|
||||||
|
System.Net.Sockets.AddressFamily.InterNetwork => 32,
|
||||||
|
System.Net.Sockets.AddressFamily.InterNetworkV6 => 128,
|
||||||
|
_ => throw new ArgumentException($"Unsupported address family '{baseAddress.AddressFamily}'.", nameof(baseAddress)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (prefixLength < 0 || prefixLength > maxPrefix)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(prefixLength), $"Prefix length must be between 0 and {maxPrefix}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NftValue(
|
||||||
|
NftValueKind.Network,
|
||||||
|
baseAddress + "/" + prefixLength.ToString(CultureInfo.InvariantCulture),
|
||||||
|
baseAddress.AddressFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a transport port value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port">The TCP/UDP port.</param>
|
||||||
|
/// <returns>A typed value for the port.</returns>
|
||||||
|
public static NftValue Port(ushort port)
|
||||||
|
=> new(NftValueKind.Port, port.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an interface-name value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The interface name.</param>
|
||||||
|
/// <returns>A typed value for the interface.</returns>
|
||||||
|
public static NftValue Interface(string name)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||||
|
string escaped = name.Replace("\\", "\\\\", StringComparison.Ordinal)
|
||||||
|
.Replace("\"", "\\\"", StringComparison.Ordinal);
|
||||||
|
return new NftValue(NftValueKind.Interface, $"\"{escaped}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a packet mark value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mark">The mark value.</param>
|
||||||
|
/// <returns>A typed value for the mark.</returns>
|
||||||
|
public static NftValue Mark(uint mark)
|
||||||
|
=> new(NftValueKind.Mark, mark.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a verdict value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="verdict">The verdict.</param>
|
||||||
|
/// <returns>A typed value for the verdict token.</returns>
|
||||||
|
public static NftValue Verdict(NftVerdict verdict)
|
||||||
|
=> new(NftValueKind.Verdict, verdict switch
|
||||||
|
{
|
||||||
|
NftVerdict.Accept => "accept",
|
||||||
|
NftVerdict.Drop => "drop",
|
||||||
|
NftVerdict.Reject => "reject",
|
||||||
|
NftVerdict.Jump => throw new ArgumentException("Jump verdicts require a chain target and are not valid as standalone typed values.", nameof(verdict)),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(verdict), verdict, "Unsupported verdict value."),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString() => RenderedText;
|
||||||
|
}
|
||||||
42
src/LibNftables/NftValueKind.cs
Normal file
42
src/LibNftables/NftValueKind.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the kind of typed nftables value represented by <see cref="NftValue"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum NftValueKind
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A raw nft literal token.
|
||||||
|
/// </summary>
|
||||||
|
Raw,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A single IP address.
|
||||||
|
/// </summary>
|
||||||
|
Address,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An IP network/CIDR value.
|
||||||
|
/// </summary>
|
||||||
|
Network,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A transport port/service value.
|
||||||
|
/// </summary>
|
||||||
|
Port,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interface name.
|
||||||
|
/// </summary>
|
||||||
|
Interface,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A packet mark value.
|
||||||
|
/// </summary>
|
||||||
|
Mark,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A verdict token.
|
||||||
|
/// </summary>
|
||||||
|
Verdict,
|
||||||
|
}
|
||||||
27
src/LibNftables/NftVerdict.cs
Normal file
27
src/LibNftables/NftVerdict.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace LibNftables;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported verdict actions for the typed high-level rule model.
|
||||||
|
/// </summary>
|
||||||
|
public enum NftVerdict
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Accept the packet.
|
||||||
|
/// </summary>
|
||||||
|
Accept,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drop the packet.
|
||||||
|
/// </summary>
|
||||||
|
Drop,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reject the packet.
|
||||||
|
/// </summary>
|
||||||
|
Reject,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Jump to another chain.
|
||||||
|
/// </summary>
|
||||||
|
Jump,
|
||||||
|
}
|
||||||
@@ -63,6 +63,10 @@ public sealed class NftablesClient : INftablesClient
|
|||||||
return Validate(CreateTypedRequest(ruleset), ct);
|
return Validate(CreateTypedRequest(ruleset), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string RenderRuleset(NftRuleset ruleset)
|
||||||
|
=> NftRulesetRenderer.Render(ruleset);
|
||||||
|
|
||||||
/// <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));
|
||||||
|
|||||||
@@ -3,53 +3,30 @@ namespace LibNftables.Tests;
|
|||||||
public sealed class NftRulesetRendererTests
|
public sealed class NftRulesetRendererTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Render_WithTypedSet_RendersExpectedCommands()
|
public void Render_WithTypedRuleAndMap_RendersExpectedCommands()
|
||||||
{
|
{
|
||||||
var ruleset = new NftRuleset();
|
NftRuleset ruleset = CreateTypedRuleset();
|
||||||
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);
|
string rendered = NftRulesetRenderer.Render(ruleset);
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
"add table inet filter" + Environment.NewLine +
|
"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 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,
|
||||||
rendered);
|
rendered);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Render_WithCustomTypeExpression_UsesCustomKeyword()
|
public void Render_WithRenderHelperReadyRuleset_RendersMapValueVerdicts()
|
||||||
{
|
{
|
||||||
var ruleset = new NftRuleset();
|
NftRuleset ruleset = CreateTypedRuleset();
|
||||||
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);
|
string rendered = NftRulesetRenderer.Render(ruleset);
|
||||||
|
|
||||||
Assert.Contains("type inet_service;", rendered, StringComparison.Ordinal);
|
Assert.Contains("80 : accept", rendered, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("443 : drop", rendered, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -58,6 +35,58 @@ public sealed class NftRulesetRendererTests
|
|||||||
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(new NftRuleset()));
|
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(new NftRuleset()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Render_WithPortMatchWithoutProtocol_ThrowsValidationException()
|
||||||
|
{
|
||||||
|
var ruleset = new NftRuleset();
|
||||||
|
var table = new NftTable
|
||||||
|
{
|
||||||
|
Name = "filter",
|
||||||
|
};
|
||||||
|
var chain = new NftChain
|
||||||
|
{
|
||||||
|
Name = "input",
|
||||||
|
Type = NftChainType.Filter,
|
||||||
|
Hook = NftHook.Input,
|
||||||
|
Priority = 0,
|
||||||
|
};
|
||||||
|
chain.Rules.Add(new NftRule
|
||||||
|
{
|
||||||
|
DestinationPort = NftValue.Port(22),
|
||||||
|
Verdict = NftVerdict.Accept,
|
||||||
|
});
|
||||||
|
table.Chains.Add(chain);
|
||||||
|
ruleset.Tables.Add(table);
|
||||||
|
|
||||||
|
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(ruleset));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Render_WithUnknownJumpTarget_ThrowsValidationException()
|
||||||
|
{
|
||||||
|
var ruleset = new NftRuleset();
|
||||||
|
var table = new NftTable
|
||||||
|
{
|
||||||
|
Name = "filter",
|
||||||
|
};
|
||||||
|
var chain = new NftChain
|
||||||
|
{
|
||||||
|
Name = "input",
|
||||||
|
Type = NftChainType.Filter,
|
||||||
|
Hook = NftHook.Input,
|
||||||
|
Priority = 0,
|
||||||
|
};
|
||||||
|
chain.Rules.Add(new NftRule
|
||||||
|
{
|
||||||
|
Verdict = NftVerdict.Jump,
|
||||||
|
JumpTarget = "missing",
|
||||||
|
});
|
||||||
|
table.Chains.Add(chain);
|
||||||
|
ruleset.Tables.Add(table);
|
||||||
|
|
||||||
|
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(ruleset));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Render_WithConflictingSetTypeSources_ThrowsValidationException()
|
public void Render_WithConflictingSetTypeSources_ThrowsValidationException()
|
||||||
{
|
{
|
||||||
@@ -77,23 +106,61 @@ public sealed class NftRulesetRendererTests
|
|||||||
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(ruleset));
|
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(ruleset));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
private static NftRuleset CreateTypedRuleset()
|
||||||
public void Render_WithInvalidElement_ThrowsValidationException()
|
|
||||||
{
|
{
|
||||||
var ruleset = new NftRuleset();
|
var ruleset = new NftRuleset();
|
||||||
var table = new NftTable
|
var table = new NftTable
|
||||||
{
|
{
|
||||||
|
Family = NftFamily.Inet,
|
||||||
Name = "filter",
|
Name = "filter",
|
||||||
};
|
};
|
||||||
|
|
||||||
var set = new NftSet
|
var set = new NftSet
|
||||||
{
|
{
|
||||||
Name = "bad",
|
Name = "blocked_ipv4",
|
||||||
Type = NftSetType.Mark,
|
Type = NftSetType.Ipv4Address,
|
||||||
};
|
};
|
||||||
set.Elements.Add("1; drop");
|
set.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.1")));
|
||||||
|
set.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.2")));
|
||||||
table.Sets.Add(set);
|
table.Sets.Add(set);
|
||||||
ruleset.Tables.Add(table);
|
|
||||||
|
|
||||||
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(ruleset));
|
var map = new NftMap
|
||||||
|
{
|
||||||
|
Name = "service_policy",
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
table.Maps.Add(map);
|
||||||
|
|
||||||
|
var chain = new NftChain
|
||||||
|
{
|
||||||
|
Name = "input",
|
||||||
|
Type = NftChainType.Filter,
|
||||||
|
Hook = NftHook.Input,
|
||||||
|
Priority = 0,
|
||||||
|
Policy = NftChainPolicy.Drop,
|
||||||
|
};
|
||||||
|
chain.Rules.Add(new NftRule
|
||||||
|
{
|
||||||
|
InputInterface = NftValue.Interface("eth0"),
|
||||||
|
SourceAddressSetName = "blocked_ipv4",
|
||||||
|
TransportProtocol = NftTransportProtocol.Tcp,
|
||||||
|
DestinationPort = NftValue.Port(22),
|
||||||
|
Verdict = NftVerdict.Accept,
|
||||||
|
});
|
||||||
|
table.Chains.Add(chain);
|
||||||
|
|
||||||
|
ruleset.Tables.Add(table);
|
||||||
|
return ruleset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,9 +91,27 @@ public sealed class NftablesClientIntegrationTests
|
|||||||
Name = "blocked_ipv4",
|
Name = "blocked_ipv4",
|
||||||
Type = NftSetType.Ipv4Address,
|
Type = NftSetType.Ipv4Address,
|
||||||
};
|
};
|
||||||
set.Elements.Add("10.0.0.1");
|
set.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.1")));
|
||||||
set.Elements.Add("10.0.0.2");
|
set.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.2")));
|
||||||
table.Sets.Add(set);
|
table.Sets.Add(set);
|
||||||
|
|
||||||
|
var chain = new NftChain
|
||||||
|
{
|
||||||
|
Name = "input",
|
||||||
|
Type = NftChainType.Filter,
|
||||||
|
Hook = NftHook.Input,
|
||||||
|
Priority = 0,
|
||||||
|
Policy = NftChainPolicy.Drop,
|
||||||
|
};
|
||||||
|
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);
|
ruleset.Tables.Add(table);
|
||||||
|
|
||||||
NftValidationResult result = client.ValidateRuleset(ruleset);
|
NftValidationResult result = client.ValidateRuleset(ruleset);
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ public sealed class NftablesClientUnitTests
|
|||||||
Assert.True(context.DryRun);
|
Assert.True(context.DryRun);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
"add table inet filter" + Environment.NewLine +
|
"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 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,
|
||||||
context.LastCommandText);
|
context.LastCommandText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +98,10 @@ public sealed class NftablesClientUnitTests
|
|||||||
Assert.False(context.DryRun);
|
Assert.False(context.DryRun);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
"add table inet filter" + Environment.NewLine +
|
"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 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,
|
||||||
context.LastCommandText);
|
context.LastCommandText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,10 +115,31 @@ public sealed class NftablesClientUnitTests
|
|||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
"add table inet filter" + Environment.NewLine +
|
"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 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,
|
||||||
context.LastCommandText);
|
context.LastCommandText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderRuleset_ReturnsTypedRulesetTextWithoutExecuting()
|
||||||
|
{
|
||||||
|
var context = new FakeContext();
|
||||||
|
var client = CreateClient(() => context);
|
||||||
|
|
||||||
|
string rendered = client.RenderRuleset(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 +
|
||||||
|
"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,
|
||||||
|
rendered);
|
||||||
|
Assert.Null(context.LastCommandText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Apply_WithFileRequest_UsesFileExecutionPath()
|
public void Apply_WithFileRequest_UsesFileExecutionPath()
|
||||||
{
|
{
|
||||||
@@ -248,14 +275,52 @@ public sealed class NftablesClientUnitTests
|
|||||||
Family = NftFamily.Inet,
|
Family = NftFamily.Inet,
|
||||||
Name = "filter",
|
Name = "filter",
|
||||||
};
|
};
|
||||||
|
|
||||||
var set = new NftSet
|
var set = new NftSet
|
||||||
{
|
{
|
||||||
Name = "blocked_ipv4",
|
Name = "blocked_ipv4",
|
||||||
Type = NftSetType.Ipv4Address,
|
Type = NftSetType.Ipv4Address,
|
||||||
};
|
};
|
||||||
set.Elements.Add("10.0.0.1");
|
set.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.1")));
|
||||||
set.Elements.Add("10.0.0.2");
|
set.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.2")));
|
||||||
table.Sets.Add(set);
|
table.Sets.Add(set);
|
||||||
|
|
||||||
|
var map = new NftMap
|
||||||
|
{
|
||||||
|
Name = "service_policy",
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
table.Maps.Add(map);
|
||||||
|
|
||||||
|
var chain = new NftChain
|
||||||
|
{
|
||||||
|
Name = "input",
|
||||||
|
Type = NftChainType.Filter,
|
||||||
|
Hook = NftHook.Input,
|
||||||
|
Priority = 0,
|
||||||
|
Policy = NftChainPolicy.Drop,
|
||||||
|
};
|
||||||
|
chain.Rules.Add(new NftRule
|
||||||
|
{
|
||||||
|
InputInterface = NftValue.Interface("eth0"),
|
||||||
|
SourceAddressSetName = "blocked_ipv4",
|
||||||
|
TransportProtocol = NftTransportProtocol.Tcp,
|
||||||
|
DestinationPort = NftValue.Port(22),
|
||||||
|
Verdict = NftVerdict.Accept,
|
||||||
|
});
|
||||||
|
table.Chains.Add(chain);
|
||||||
|
|
||||||
ruleset.Tables.Add(table);
|
ruleset.Tables.Add(table);
|
||||||
return ruleset;
|
return ruleset;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user