Expand typed firewall and map API

This commit is contained in:
Vibe Myass
2026-03-16 04:07:08 +00:00
parent 1dfc6aebfd
commit e89739a64f
21 changed files with 1373 additions and 131 deletions

View File

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

View File

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

View 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>();
}

View 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,
}

View 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,
}

View 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
View 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>();
}

View 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; }
}

View 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,
}

View 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; }
}

View File

@@ -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),
};
}

View File

@@ -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>();
}

View File

@@ -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>();
}

View 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
View 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;
}

View 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,
}

View 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,
}

View File

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

View File

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

View File

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

View File

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