On Thu, Dec 11, 2025 at 03:00:49PM +0100, Dion Bosschieter wrote:
> This series aims to implement nftables as a backend driver for
> the nwfilter feature. The idea is that eventually it will replace
> the ebiptables driver and provide an easy way for users to switch
> from one driver to another.
>
> The first 2 patches are moving of functions and renames, meant to decouple
> nwfilter from the currently only existing ebiptables driver.
>
> The 3rd patch introduces the new nwfilter driver. After which nwfilter allows
> users to choose it in the 4th patch.
>
> The last patch introduces unit testing of the new nftables driver.
>
> - Resolves issue https://gitlab.com/libvirt/libvirt/-/issues/603
> benchmarks showed that the amount of iifname jumps for each
> interface with is the cause for this.
> Switched the nftables driver towards a vmap (verdict map) so we
> can have 1 rule that jumps to the correct root input/output chain
> per interface. Which improves throughput as when the number of
> interface check and jump rules increases the throughput decreases.
> The issue describes the interface matching works using the interface
> name and the majority of the effort is the strncpy, this commit also
> switches nftables to an interface_index compare instead.
> However, just using the interface_index is not enough, the amount of
> oif and iif jump rules causes quite a performance issue,
> the vmap instead solves this.
That's good.
> - Split rules into separate tables: "libvirt-nwfilter-ethernet" and
> "libvirt-nwfilter-other" to preserve existing firewall behavior.
> - Stuck with prerouting and postrouting as hooks for input / output
> on the -ethernet and -other table. This makes it easier to merge
> the tables in the future. Saving management of two tables and
> decreasing the amount of tables a packet sees. Currently ebtables
> filtering happens via PREROUTING and POSTROUTING hooks, while
> ip/ip6tables filtering happens in the output/forward hooks.
> - Stuck with 2 tables for compatibility reasons with eb iptables,
> unifying into 1 table will break users firewall definitions, which
> depend on being able to do accepts on ethernet rules
> (which currently get defined via ebtables) and additional filtering
> via the ip rules (which currently get defined via ip(6)tables).
> The nwfilter_nftables_driver keeps splitting the ethernet and
> non ethernet (other) rules in seperate tables
> "libvirt-nwfilter-ethernet" and "libvirt-nwfilter-other".
I guess with xtables, we would have effectively three - ebtables,
iptables and ip6tables. "other" here covers both iptables and
ip6tables which is fine because rules for those are mutually
exclusive for any single packet. Perhaps call it "-inet" instead
of '-other' ?
> Unsupported nwfilter features (for now):
> - STP filtering
> - Gratuitous ARP filtering
> - IPSets (potential future support via nft sets)
> - Reject due to filtering in pre/postrouting, using drop instead
> of reject, copying logic from existing ebiptables ebtables actions
I don't know if it is related, but when I tried this patch series
on an existing VM configured with the "clean-traffic" filter I
get a failure:
2026-01-16 16:24:14.492+0000: 749946: error : virFirewallCmdNftablesApply:748 : internal error: Failed to apply firewall command 'nft add rule bridge libvirt-nwfilter-ethernet n-vnet0-rarp-out ether saddr == 52:54:00:5e:58:55 ether daddr == ff:ff:ff:ff:ff:ff ether type 0x8035 'arp operation' 3 arp saddr ip 0.0.0.0/32 arp daddr ip 0.0.0.0/32 'ether saddr' 52:54:00:5e:58:55 'ether daddr' 52:54:00:5e:58:55 accept comment '"priority=500"'': Error: conflicting statements
nft add rule bridge libvirt-nwfilter-ethernet n-vnet0-rarp-out \
ether saddr == 52:54:00:5e:58:55 \
ether daddr == ff:ff:ff:ff:ff:ff \
ether type 0x8035 \
'arp operation' 3 \
arp saddr ip 0.0.0.0/32 \
arp daddr ip 0.0.0.0/32 \
'ether saddr' 52:54:00:5e:58:55 \
'ether daddr' 52:54:00:5e:58:55 \
accept comment '"priority=500"'
We seem to have generated the MAC address matches twice resulting
in the 'conflicting statements' message.
Also those extra single quotes are a sign of a bug in passing the
args. Two nft args are being passed as a single argv element.
Interestingly that didn't seem to cause any problem, making me
think nft is munging all argv back to a single string and then
parsing it.
The following would fix it
diff --git a/src/nwfilter/nwfilter_nftables_driver.c b/src/nwfilter/nwfilter_nftables_driver.c
index 36a6c63f22..0ccb765a4e 100644
--- a/src/nwfilter/nwfilter_nftables_driver.c
+++ b/src/nwfilter/nwfilter_nftables_driver.c
@@ -460,7 +460,8 @@ insertRuleArg2Param(virFirewall *fw,
virNWFilterVarCombIter *vars,
nwItemDesc *itemLow,
nwItemDesc *itemHigh,
- const char *argument,
+ const char *argument1,
+ const char *argument2,
const char *seperator)
{
char field[VIR_INT64_STR_BUFLEN];
@@ -471,7 +472,8 @@ insertRuleArg2Param(virFirewall *fw,
field, sizeof(field),
itemLow) < 0)
return -1;
- virFirewallCmdAddArg(fw, fwrule, argument);
+ virFirewallCmdAddArg(fw, fwrule, argument1);
+ virFirewallCmdAddArg(fw, fwrule, argument2);
if (ENTRY_WANT_NEG_SIGN(itemLow))
virFirewallCmdAddArg(fw, fwrule, "!=");
if (HAS_ENTRY_ITEM(itemHigh)) {
@@ -497,21 +499,15 @@ nftablesHandlePortData(virFirewall *fw,
portDataDef *portData,
bool reverseRule)
{
- char dport[VIR_INT64_STR_BUFLEN];
- char sport[VIR_INT64_STR_BUFLEN];
-
- g_snprintf(dport, sizeof(dport), reverseRule ? "%s sport" : "%s dport",
- protocol);
- g_snprintf(sport, sizeof(sport), reverseRule ? "%s dport": "%s sport",
- protocol);
-
if (insertRuleArg2Param(fw, fwrule, vars,
&portData->dataDstPortStart,
- &portData->dataDstPortEnd, dport, "-") < 0)
+ &portData->dataDstPortEnd, protocol,
+ reverseRule ? "sport" : "dport", "-") < 0)
return -1;
if (insertRuleArg2Param(fw, fwrule, vars,
&portData->dataSrcPortStart,
- &portData->dataSrcPortEnd, sport, "-") < 0)
+ &portData->dataSrcPortEnd, protocol,
+ reverseRule ? "dport" : "sport", "-") < 0)
return -1;
return 0;
@@ -522,7 +518,8 @@ nftablesHandleMacAddr(virFirewall *fw,
virFirewallCmd *fwrule,
virNWFilterVarCombIter *vars,
nwItemDesc *macaddr,
- const char *argument)
+ const char *argument1,
+ const char *argument2)
{
char macstr[VIR_MAC_STRING_BUFLEN];
@@ -532,7 +529,8 @@ nftablesHandleMacAddr(virFirewall *fw,
macaddr) < 0)
return -1;
- virFirewallCmdAddArg(fw, fwrule, argument);
+ virFirewallCmdAddArg(fw, fwrule, argument1);
+ virFirewallCmdAddArg(fw, fwrule, argument2);
if (ENTRY_WANT_NEG_SIGN(macaddr))
virFirewallCmdAddArg(fw, fwrule, "!=");
virFirewallCmdAddArg(fw, fwrule, macstr);
@@ -547,7 +545,7 @@ nftablesHandleSrcMacAddr(virFirewall *fw,
virNWFilterVarCombIter *vars,
nwItemDesc *srcMacAddr)
{
- return nftablesHandleMacAddr(fw, fwrule, vars, srcMacAddr, "ether saddr");
+ return nftablesHandleMacAddr(fw, fwrule, vars, srcMacAddr, "ether", "saddr");
}
static void
@@ -893,7 +891,8 @@ insertRuleArgParam(virFirewall *fw,
virFirewallCmd *fwrule,
virNWFilterVarCombIter *vars,
nwItemDesc *item,
- const char *argument)
+ const char *argument1,
+ const char *argument2)
{
char field[VIR_INT64_STR_BUFLEN];
@@ -902,7 +901,8 @@ insertRuleArgParam(virFirewall *fw,
field, sizeof(field),
item) < 0)
return -1;
- virFirewallCmdAddArg(fw, fwrule, argument);
+ virFirewallCmdAddArg(fw, fwrule, argument1);
+ virFirewallCmdAddArg(fw, fwrule, argument2);
if (ENTRY_WANT_NEG_SIGN(item))
virFirewallCmdAddArg(fw, fwrule, "!=");
@@ -917,7 +917,8 @@ insertRuleArgParamHex(virFirewall *fw,
virFirewallCmd *fwrule,
virNWFilterVarCombIter *vars,
nwItemDesc *item,
- const char *argument)
+ const char *argument1,
+ const char *argument2)
{
char field[VIR_INT64_STR_BUFLEN];
@@ -926,7 +927,8 @@ insertRuleArgParamHex(virFirewall *fw,
field, sizeof(field),
item) < 0)
return -1;
- virFirewallCmdAddArg(fw, fwrule, argument);
+ virFirewallCmdAddArg(fw, fwrule, argument1);
+ virFirewallCmdAddArg(fw, fwrule, argument2);
if (ENTRY_WANT_NEG_SIGN(item))
virFirewallCmdAddArg(fw, fwrule, "!=");
@@ -971,7 +973,7 @@ nftablesHandleEthernetRule(virFirewall *fw,
if (insertRuleArgParamHex(fw, fwrule, vars,
&rule->p.ethHdrFilter.dataProtocolID,
- "ether type") < 0)
+ "ether", "type") < 0)
return -1;
break;
case VIR_NWFILTER_RULE_PROTOCOL_IP:
@@ -1031,21 +1033,21 @@ nftablesHandleEthernetRule(virFirewall *fw,
if (insertRuleArgParam(fw, fwrule, vars,
&rule->p.ipHdrFilter.ipHdr.dataProtocolID,
- "ip protocol") < 0)
+ "ip", "protocol") < 0)
return -1;
if (insertRuleArg2Param(fw, fwrule, vars,
&rule->p.ipHdrFilter.portData.dataSrcPortStart,
&rule->p.ipHdrFilter.portData.dataSrcPortEnd,
- reverseRule ? "th dport" : "th sport", "-") < 0)
+ "th", reverseRule ? "dport" : "sport", "-") < 0)
return -1;
if (insertRuleArg2Param(fw, fwrule, vars,
&rule->p.ipHdrFilter.portData.dataDstPortStart,
&rule->p.ipHdrFilter.portData.dataDstPortEnd,
- reverseRule ? "th sport" : "th dport", "-") < 0)
+ "th", reverseRule ? "sport" : "dport", "-") < 0)
return -1;
if (insertRuleArgParamHex(fw, fwrule, vars,
&rule->p.ipHdrFilter.ipHdr.dataDSCP,
- "ip dscp") < 0)
+ "ip", "dscp") < 0)
return -1;
break;
case VIR_NWFILTER_RULE_PROTOCOL_ARP:
@@ -1063,15 +1065,15 @@ nftablesHandleEthernetRule(virFirewall *fw,
if (insertRuleArgParam(fw, fwrule, vars,
&rule->p.arpHdrFilter.dataHWType,
- "arp htype") < 0)
+ "arp", "htype") < 0)
return -1;
if (insertRuleArgParam(fw, fwrule, vars,
&rule->p.arpHdrFilter.dataOpcode,
- "arp operation") < 0)
+ "arp", "operation") < 0)
return -1;
if (insertRuleArgParamHex(fw, fwrule, vars,
&rule->p.arpHdrFilter.dataProtocolType,
- "arp ptype") < 0)
+ "arp", "ptype") < 0)
return -1;
if (HAS_ENTRY_ITEM(&rule->p.arpHdrFilter.dataARPSrcIPAddr)) {
@@ -1118,11 +1120,11 @@ nftablesHandleEthernetRule(virFirewall *fw,
if (nftablesHandleMacAddr(fw, fwrule, vars,
&rule->p.arpHdrFilter.dataARPSrcMACAddr,
- reverseRule ? "ether daddr": "ether saddr") < 0)
+ "ether", reverseRule ? "addr": "saddr") < 0)
return -1;
if (nftablesHandleMacAddr(fw, fwrule, vars,
&rule->p.arpHdrFilter.dataARPDstMACAddr,
- reverseRule ? "ether saddr": "ether daddr") < 0)
+ "ether", reverseRule ? "saddr": "daddr") < 0)
return -1;
if (HAS_ENTRY_ITEM(&rule->p.arpHdrFilter.dataGratuitousARP) &&
@@ -1187,28 +1189,28 @@ nftablesHandleEthernetRule(virFirewall *fw,
if (insertRuleArgParam(fw, fwrule, vars,
&rule->p.ipv6HdrFilter.ipHdr.dataProtocolID,
- "ip6 nexthdr") < 0)
+ "ip6", "nexthdr") < 0)
return -1;
if (insertRuleArg2Param(fw, fwrule, vars,
&rule->p.ipv6HdrFilter.portData.dataSrcPortStart,
&rule->p.ipv6HdrFilter.portData.dataSrcPortEnd,
- reverseRule ? "th dport" : "th sport", "-") < 0)
+ "th", reverseRule ? "dport" : "sport", "-") < 0)
return -1;
if (insertRuleArg2Param(fw, fwrule, vars,
&rule->p.ipv6HdrFilter.portData.dataDstPortStart,
&rule->p.ipv6HdrFilter.portData.dataDstPortEnd,
- reverseRule ? "th sport" : "th dport", "-") < 0)
+ "th", reverseRule ? "sport" : "dport", "-") < 0)
return -1;
if (HAS_ENTRY_ITEM(&rule->p.ipv6HdrFilter.dataICMPTypeStart) ||
HAS_ENTRY_ITEM(&rule->p.ipv6HdrFilter.dataICMPCodeStart)) {
if (insertRuleArgParam(fw, fwrule, vars,
&rule->p.ipv6HdrFilter.dataICMPTypeStart,
- "icmpv6 type") < 0)
+ "icmpv6", "type") < 0)
return -1;
if (insertRuleArgParam(fw, fwrule, vars,
&rule->p.ipv6HdrFilter.dataICMPCodeStart,
- "icmpv6 code") < 0)
+ "icmpv6", "code") < 0)
return -1;
}
break;
@@ -1222,11 +1224,11 @@ nftablesHandleEthernetRule(virFirewall *fw,
if (insertRuleArgParam(fw, fwrule, vars,
&rule->p.vlanHdrFilter.dataVlanID,
- "vlan id") < 0)
+ "vlan", "id") < 0)
return -1;
if (insertRuleArgParam(fw, fwrule, vars,
&rule->p.vlanHdrFilter.dataVlanEncap,
- "vlan type") < 0)
+ "vlan", "type") < 0)
return -1;
break;
case VIR_NWFILTER_RULE_PROTOCOL_STP:
With regards,
Daniel
--
|: https://berrange.com -o- https://www.flickr.com/photos/dberrange :|
|: https://libvirt.org -o- https://fstop138.berrange.com :|
|: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|