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.
|
||||
|
||||
- High-level managed API:
|
||||
- typed `NftRuleset` / `NftTable` / `NftSet` authoring
|
||||
- typed `NftRuleset` / `NftTable` / `NftSet` / `NftMap` / `NftChain` / `NftRule` authoring
|
||||
- `RenderRuleset`
|
||||
- `ValidateRuleset`
|
||||
- `ApplyRuleset`
|
||||
- `Snapshot`
|
||||
@@ -17,7 +18,7 @@ This library is intentionally narrow.
|
||||
|
||||
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
|
||||
- Cross-platform support beyond Linux x64
|
||||
|
||||
@@ -79,11 +80,31 @@ var blocked = new NftSet
|
||||
Name = "blocked_ipv4",
|
||||
Type = NftSetType.Ipv4Address,
|
||||
};
|
||||
blocked.Elements.Add("10.0.0.1");
|
||||
blocked.Elements.Add("10.0.0.2");
|
||||
blocked.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.1")));
|
||||
blocked.Elements.Add(NftValue.Address(System.Net.IPAddress.Parse("10.0.0.2")));
|
||||
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);
|
||||
|
||||
string preview = client.RenderRuleset(ruleset);
|
||||
var validation = client.ValidateRuleset(ruleset);
|
||||
if (validation.IsValid)
|
||||
{
|
||||
|
||||
@@ -31,6 +31,14 @@ public interface INftablesClient
|
||||
/// <exception cref="NftNativeLoadException">Thrown when required native runtime components cannot be loaded.</exception>
|
||||
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>
|
||||
/// Asynchronously validates a ruleset request using dry-run mode.
|
||||
/// </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;
|
||||
|
||||
namespace LibNftables;
|
||||
@@ -32,60 +33,302 @@ internal static class NftRulesetRenderer
|
||||
null);
|
||||
}
|
||||
|
||||
builder.Append("add table ")
|
||||
.Append(ToKeyword(table.Family))
|
||||
.Append(' ')
|
||||
.Append(table.Name)
|
||||
.AppendLine();
|
||||
|
||||
var setNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (NftSet set in table.Sets)
|
||||
{
|
||||
ValidateSet(set, table);
|
||||
|
||||
if (!setNames.Add(set.Name!))
|
||||
{
|
||||
throw new NftValidationException(
|
||||
$"Table '{table.Name}' contains duplicate set '{set.Name}'.",
|
||||
0,
|
||||
null);
|
||||
}
|
||||
|
||||
builder.Append("add set ")
|
||||
.Append(ToKeyword(table.Family))
|
||||
.Append(' ')
|
||||
.Append(table.Name)
|
||||
.Append(' ')
|
||||
.Append(set.Name)
|
||||
.Append(" { type ")
|
||||
.Append(GetTypeExpression(set))
|
||||
.Append(';');
|
||||
|
||||
if (set.Elements.Count > 0)
|
||||
{
|
||||
builder.Append(" elements = { ");
|
||||
|
||||
for (int i = 0; i < set.Elements.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
|
||||
builder.Append(set.Elements[i]);
|
||||
}
|
||||
|
||||
builder.Append(" };");
|
||||
}
|
||||
|
||||
builder.Append(" }")
|
||||
.AppendLine();
|
||||
}
|
||||
RenderTable(builder, table);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (table is null)
|
||||
@@ -100,42 +343,236 @@ internal static class NftRulesetRenderer
|
||||
{
|
||||
if (set is null)
|
||||
{
|
||||
throw new NftValidationException(
|
||||
$"Table '{table.Name}' contains a null set definition.",
|
||||
0,
|
||||
null);
|
||||
throw new NftValidationException($"Table '{table.Name}' contains a null set definition.", 0, null);
|
||||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
if (!string.IsNullOrWhiteSpace(set.CustomTypeExpression))
|
||||
{
|
||||
ValidateToken(set.CustomTypeExpression!, $"Set '{set.Name}' custom type");
|
||||
}
|
||||
|
||||
for (int i = 0; i < set.Elements.Count; i++)
|
||||
{
|
||||
string? element = set.Elements[i];
|
||||
if (string.IsNullOrWhiteSpace(element))
|
||||
if (set.Elements[i] is null)
|
||||
{
|
||||
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(
|
||||
$"Set '{set.Name}' contains an empty element at index {i}.",
|
||||
$"Chain '{chain.Name}' rule at index {index} jumps to unknown chain '{rule.JumpTarget}'.",
|
||||
0,
|
||||
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 '\'')
|
||||
{
|
||||
throw new NftValidationException(
|
||||
$"{fieldName} contains unsupported character '{ch}'.",
|
||||
0,
|
||||
null);
|
||||
throw new NftValidationException($"{fieldName} contains unsupported character '{ch}'.", 0, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,15 +598,63 @@ internal static class NftRulesetRenderer
|
||||
{
|
||||
if (ch is '\r' or '\n' or ';')
|
||||
{
|
||||
throw new NftValidationException(
|
||||
$"{fieldName} contains unsupported character '{ch}'.",
|
||||
0,
|
||||
null);
|
||||
throw new NftValidationException($"{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
|
||||
{
|
||||
NftSetType.Ipv4Address => "ipv4_addr",
|
||||
@@ -184,6 +666,24 @@ internal static class NftRulesetRenderer
|
||||
_ => 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)
|
||||
=> family switch
|
||||
{
|
||||
@@ -195,4 +695,41 @@ internal static class NftRulesetRenderer
|
||||
NftFamily.Netdev => "netdev",
|
||||
_ => 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.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RenderRuleset(NftRuleset ruleset)
|
||||
=> NftRulesetRenderer.Render(ruleset);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<NftValidationResult> ValidateAsync(NftApplyRequest request, CancellationToken ct = default)
|
||||
=> Task.FromResult(Validate(request, ct));
|
||||
|
||||
@@ -3,53 +3,30 @@ namespace LibNftables.Tests;
|
||||
public sealed class NftRulesetRendererTests
|
||||
{
|
||||
[Fact]
|
||||
public void Render_WithTypedSet_RendersExpectedCommands()
|
||||
public void Render_WithTypedRuleAndMap_RendersExpectedCommands()
|
||||
{
|
||||
var ruleset = new NftRuleset();
|
||||
var table = new NftTable
|
||||
{
|
||||
Family = NftFamily.Inet,
|
||||
Name = "filter",
|
||||
};
|
||||
var set = new NftSet
|
||||
{
|
||||
Name = "blocked_ipv4",
|
||||
Type = NftSetType.Ipv4Address,
|
||||
};
|
||||
set.Elements.Add("10.0.0.1");
|
||||
set.Elements.Add("10.0.0.2");
|
||||
table.Sets.Add(set);
|
||||
ruleset.Tables.Add(table);
|
||||
NftRuleset ruleset = CreateTypedRuleset();
|
||||
|
||||
string rendered = NftRulesetRenderer.Render(ruleset);
|
||||
|
||||
Assert.Equal(
|
||||
"add table inet filter" + Environment.NewLine +
|
||||
"add set inet filter blocked_ipv4 { type ipv4_addr; elements = { 10.0.0.1, 10.0.0.2 }; }" + Environment.NewLine,
|
||||
"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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_WithCustomTypeExpression_UsesCustomKeyword()
|
||||
public void Render_WithRenderHelperReadyRuleset_RendersMapValueVerdicts()
|
||||
{
|
||||
var ruleset = new NftRuleset();
|
||||
var table = new NftTable
|
||||
{
|
||||
Family = NftFamily.Ip,
|
||||
Name = "custom",
|
||||
};
|
||||
var set = new NftSet
|
||||
{
|
||||
Name = "ports",
|
||||
CustomTypeExpression = "inet_service",
|
||||
};
|
||||
set.Elements.Add("80");
|
||||
table.Sets.Add(set);
|
||||
ruleset.Tables.Add(table);
|
||||
NftRuleset ruleset = CreateTypedRuleset();
|
||||
|
||||
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]
|
||||
@@ -58,6 +35,58 @@ public sealed class NftRulesetRendererTests
|
||||
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]
|
||||
public void Render_WithConflictingSetTypeSources_ThrowsValidationException()
|
||||
{
|
||||
@@ -77,23 +106,61 @@ public sealed class NftRulesetRendererTests
|
||||
Assert.Throws<NftValidationException>(() => NftRulesetRenderer.Render(ruleset));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_WithInvalidElement_ThrowsValidationException()
|
||||
private static NftRuleset CreateTypedRuleset()
|
||||
{
|
||||
var ruleset = new NftRuleset();
|
||||
var table = new NftTable
|
||||
{
|
||||
Family = NftFamily.Inet,
|
||||
Name = "filter",
|
||||
};
|
||||
|
||||
var set = new NftSet
|
||||
{
|
||||
Name = "bad",
|
||||
Type = NftSetType.Mark,
|
||||
Name = "blocked_ipv4",
|
||||
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);
|
||||
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",
|
||||
Type = NftSetType.Ipv4Address,
|
||||
};
|
||||
set.Elements.Add("10.0.0.1");
|
||||
set.Elements.Add("10.0.0.2");
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
NftValidationResult result = client.ValidateRuleset(ruleset);
|
||||
|
||||
@@ -80,7 +80,10 @@ public sealed class NftablesClientUnitTests
|
||||
Assert.True(context.DryRun);
|
||||
Assert.Equal(
|
||||
"add table inet filter" + Environment.NewLine +
|
||||
"add set inet filter blocked_ipv4 { type ipv4_addr; elements = { 10.0.0.1, 10.0.0.2 }; }" + Environment.NewLine,
|
||||
"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);
|
||||
}
|
||||
|
||||
@@ -95,7 +98,10 @@ public sealed class NftablesClientUnitTests
|
||||
Assert.False(context.DryRun);
|
||||
Assert.Equal(
|
||||
"add table inet filter" + Environment.NewLine +
|
||||
"add set inet filter blocked_ipv4 { type ipv4_addr; elements = { 10.0.0.1, 10.0.0.2 }; }" + Environment.NewLine,
|
||||
"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);
|
||||
}
|
||||
|
||||
@@ -109,10 +115,31 @@ public sealed class NftablesClientUnitTests
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public void Apply_WithFileRequest_UsesFileExecutionPath()
|
||||
{
|
||||
@@ -248,14 +275,52 @@ public sealed class NftablesClientUnitTests
|
||||
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");
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user