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