Re: [PATCH v12 nf-next 7/7] netfilter: nft_flow_offload: Add bridgeflow to nft_flow_offload_eval()

Eric Woudstra <[email protected]>
Newsgroups gmane.linux.network.bridge,gmane.linux.network,gmane.comp.security.firewalls.netfilter.devel
Message-ID <[email protected]>

On 7/8/26 11:52 AM, Pablo Neira Ayuso wrote:
> Hi,
> 
> On Tue, Jul 07, 2026 at 11:10:45AM +0200, Eric Woudstra wrote:
>> Edit nft_flow_offload_eval() to make it possible to handle a flowtable of
>> the nft bridge family.
>>
>> Use nft_flow_offload_bridge_init() to fill the flow tuples. It uses
>> nft_dev_fill_bridge_path() in each direction.
> 
> I decided to add a bit more boiler plate in my proposal to detach the
> inet and bridge flowtable dataplanes.
> 
> More comments below.
> 
>> Signed-off-by: Eric Woudstra <[email protected]>
>> ---
>>  include/net/netfilter/nf_flow_table.h |   5 +
>>  net/netfilter/nf_flow_table_path.c    | 126 ++++++++++++++++++++++++++
>>  net/netfilter/nft_flow_offload.c      |  20 +++-
>>  3 files changed, 146 insertions(+), 5 deletions(-)
>>
>> diff --git a/include/net/netfilter/nf_flow_table.h b/include/net/netfilter/nf_flow_table.h
>> index 5c6e3b65ae85b..a109eda5250c7 100644
>> --- a/include/net/netfilter/nf_flow_table.h
>> +++ b/include/net/netfilter/nf_flow_table.h
>> @@ -305,6 +305,11 @@ nf_flow_table_offload_del_cb(struct nf_flowtable *flow_table,
>>  void flow_offload_route_init(struct flow_offload *flow,
>>  			     struct nf_flow_route *route);
>>  
>> +int flow_offload_bridge_init(struct flow_offload *flow,
>> +			     const struct nft_pktinfo *pkt,
>> +			     enum ip_conntrack_dir dir,
>> +			     struct nft_flowtable *ft);
>> +
>>  int flow_offload_add(struct nf_flowtable *flow_table, struct flow_offload *flow);
>>  void flow_offload_refresh(struct nf_flowtable *flow_table,
>>  			  struct flow_offload *flow, bool force);
>> diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
>> index 2b6ebb594a9ee..cdd6a822cb811 100644
>> --- a/net/netfilter/nf_flow_table_path.c
>> +++ b/net/netfilter/nf_flow_table_path.c
>> @@ -1,6 +1,7 @@
>>  // SPDX-License-Identifier: GPL-2.0-only
>>  #include <linux/kernel.h>
>>  #include <linux/module.h>
>> +#include <linux/if_vlan.h>
>>  #include <linux/init.h>
>>  #include <linux/etherdevice.h>
>>  #include <linux/netlink.h>
>> @@ -365,3 +366,128 @@ int nft_flow_route(const struct nft_pktinfo *pkt, const struct nf_conn *ct,
>>  	return -ENOENT;
>>  }
>>  EXPORT_SYMBOL_GPL(nft_flow_route);
>> +
>> +static int nft_dev_fill_bridge_path(struct flow_offload *flow,
>> +				    struct nft_flowtable *ft,
>> +				    enum ip_conntrack_dir dir,
>> +				    const struct net_device *src_dev,
>> +				    const struct net_device *dst_dev,
>> +				    unsigned char *src_ha,
>> +				    unsigned char *dst_ha)
>> +{
>> +	struct flow_offload_tuple_rhash *th = flow->tuplehash;
>> +	struct net_device_path_ctx ctx = {};
>> +	struct net_device_path_stack stack;
>> +	struct nft_forward_info info = {};
>> +	int i, j = 0;
>> +
>> +	for (i = th[dir].tuple.encap_num - 1; i >= 0 ; i--) {
>> +		if (info.num_encaps >= NF_FLOW_TABLE_ENCAP_MAX)
>> +			return -1;
>> +
>> +		if (th[dir].tuple.in_vlan_ingress & BIT(i))
>> +			continue;
>> +
>> +		info.encap[info.num_encaps].id = th[dir].tuple.encap[i].id;
>> +		info.encap[info.num_encaps].proto = th[dir].tuple.encap[i].proto;
>> +		info.num_encaps++;
>> +
>> +		if (th[dir].tuple.encap[i].proto == htons(ETH_P_PPP_SES))
>> +			continue;
>> +
>> +		if (ctx.num_vlans >= NET_DEVICE_PATH_VLAN_MAX)
>> +			return -1;
>> +		ctx.vlan[ctx.num_vlans].id = th[dir].tuple.encap[i].id;
>> +		ctx.vlan[ctx.num_vlans].proto = th[dir].tuple.encap[i].proto;
>> +		ctx.num_vlans++;
>> +	}
> 
> I am not sure why this is needed, in my approach I simplified this,
> but maybe I broke bridge vlan filtering. I am not sure what test
> coverage you made.
> 

This part is used for handling packets that are incoming with encaps.
Previously I got the encaps from the skb and stored them in
th[dir].tuple.encap. I understand you want to do this differently.
It is also why I'm starting in the opposite direction as in you patch-set.

The code above then reads th[dir].tuple.encap and starts filling info
and ctx with encaps. It also handles encaps marked with a
in_vlan_ingress bit. Once info and ctx are filled with the encaps from
the skb, it can continue with dev_fill_bridge_path().

My selftest script bridge_fastpath.sh tests this code thoroughly in many
different scenarios. Incoming vlan, q-in-q, ad, pppoe, pppoe-in-q. Also
on a dsa-port with the in_vlan_ingress bit set.

>> +	ctx.dev = src_dev;
>> +	ether_addr_copy(ctx.daddr, dst_ha);
>> +
>> +	if (dev_fill_bridge_path(&ctx, &stack) < 0)
>> +		return -1;
>> +
>> +	nft_dev_path_info(&stack, &info, dst_ha, &ft->data);
>> +
>> +	if (!info.indev || info.indev != dst_dev)
>> +		return -1;
>> +
>> +	th[!dir].tuple.iifidx = info.indev->ifindex;
>> +	for (i = info.num_encaps - 1; i >= 0; i--) {
>> +		th[!dir].tuple.encap[j].id = info.encap[i].id;
>> +		th[!dir].tuple.encap[j].proto = info.encap[i].proto;
>> +		if (info.ingress_vlans & BIT(i))
>> +			th[!dir].tuple.in_vlan_ingress |= BIT(j);
>> +		j++;
>> +	}
>> +	th[!dir].tuple.encap_num = info.num_encaps;
>> +
>> +	th[dir].tuple.mtu = dst_dev->mtu;
>> +	ether_addr_copy(th[dir].tuple.out.h_source, src_ha);
>> +	ether_addr_copy(th[dir].tuple.out.h_dest, dst_ha);
>> +	th[dir].tuple.out.ifidx = info.outdev->ifindex;
>> +	th[dir].tuple.xmit_type = FLOW_OFFLOAD_XMIT_DIRECT;
>> +
>> +	return 0;
>> +}
>> +
>> +int flow_offload_bridge_init(struct flow_offload *flow,
>> +			     const struct nft_pktinfo *pkt,
>> +			     enum ip_conntrack_dir dir,
>> +			     struct nft_flowtable *ft)
>> +{
>> +	const struct net_device *in_dev, *out_dev;
>> +	struct ethhdr *eth = eth_hdr(pkt->skb);
>> +	struct flow_offload_tuple *tuple;
>> +	int err, i = 0;
>> +
>> +	in_dev = nft_in(pkt);
>> +	if (!in_dev || !nft_flowtable_find_dev(in_dev, ft))
>> +		return -1;
>> +
>> +	out_dev = nft_out(pkt);
>> +	if (!out_dev || !nft_flowtable_find_dev(out_dev, ft))
>> +		return -1;
>> +
>> +	tuple =  &flow->tuplehash[!dir].tuple;
>> +
>> +	if (skb_vlan_tag_present(pkt->skb)) {
>> +		tuple->encap[i].id = skb_vlan_tag_get(pkt->skb);
>> +		tuple->encap[i].proto = pkt->skb->vlan_proto;
>> +		i++;
>> +	}
>> +
>> +	switch (eth_hdr(pkt->skb)->h_proto) {
>> +	case htons(ETH_P_8021Q): {
>> +		struct vlan_hdr *vhdr = (struct vlan_hdr *)(skb_mac_header(pkt->skb)
>> +					 + sizeof(struct ethhdr));
>> +		tuple->encap[i].id = ntohs(vhdr->h_vlan_TCI);
>> +		tuple->encap[i].proto = htons(ETH_P_8021Q);
>> +		i++;
>> +		break;
>> +	}
>> +	case htons(ETH_P_PPP_SES): {
>> +		struct pppoe_hdr *phdr = (struct pppoe_hdr *)(skb_mac_header(pkt->skb)
>> +					  + sizeof(struct ethhdr));
>> +
>> +		tuple->encap[i].id = ntohs(phdr->sid);
>> +		tuple->encap[i].proto = htons(ETH_P_PPP_SES);
>> +		i++;
>> +		break;
>> +	}
>> +	}
>> +	tuple->encap_num = i;
> 
> I am not sure these lines above can work. The VLAN tag might be
> already gone by when the packet is observed from the bridge/forward
> hook. I think populating the encap fields of the tuple by using the
> observed packet is not good to go.
> 

I have not experienced this, but I understand that this is no guarantee
that it does not happen at all.

>> +	err = nft_dev_fill_bridge_path(flow, ft, !dir, out_dev, in_dev,
>> +				       eth->h_dest, eth->h_source);
>> +	if (err < 0)
>> +		return err;
>> +
>> +	err = nft_dev_fill_bridge_path(flow, ft, dir, in_dev, out_dev,
>> +				       eth->h_source, eth->h_dest);
>> +	if (err < 0)
>> +		return err;
>> +
>> +	return 0;
>> +}
>> +EXPORT_SYMBOL_GPL(flow_offload_bridge_init);
>> diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c
>> index 0be62841155b6..d0d63ef7cecd5 100644
>> --- a/net/netfilter/nft_flow_offload.c
>> +++ b/net/netfilter/nft_flow_offload.c
>> @@ -53,6 +53,7 @@ static void nft_flow_offload_eval(const struct nft_expr *expr,
>>  {
>>  	struct nft_flow_offload *priv = nft_expr_priv(expr);
>>  	struct nf_flowtable *flowtable = &priv->flowtable->data;
>> +	bool routing = flowtable->type->family != NFPROTO_BRIDGE;
>>  	struct tcphdr _tcph, *tcph = NULL;
>>  	struct nf_flow_route route = {};
>>  	enum ip_conntrack_info ctinfo;
>> @@ -109,14 +110,21 @@ static void nft_flow_offload_eval(const struct nft_expr *expr,
>>  		goto out;
>>  
>>  	dir = CTINFO2DIR(ctinfo);
>> -	if (nft_flow_route(pkt, ct, &route, dir, priv->flowtable) < 0)
>> -		goto err_flow_route;
>> +	if (routing) {
>> +		if (nft_flow_route(pkt, ct, &route, dir, priv->flowtable) < 0)
>> +			goto err_flow_route;
>> +	}
> 
> As said, I am leaning towards adding a bit more boilerplate code to
> separate the bridge and inet flowtable datapaths.
> 
>>  	flow = flow_offload_alloc(ct);
>>  	if (!flow)
>>  		goto err_flow_alloc;
>>  
>> -	flow_offload_route_init(flow, &route);
>> +	if (routing)
>> +		flow_offload_route_init(flow, &route);
>> +	else
>> +		if (flow_offload_bridge_init(flow, pkt, dir, priv->flowtable) < 0)
>> +			goto err_flow_add;
>> +
>>  	if (tcph)
>>  		flow_offload_ct_tcp(ct);
>>  
>> @@ -164,8 +172,10 @@ static void nft_flow_offload_eval(const struct nft_expr *expr,
>>  err_flow_add:
>>  	flow_offload_free(flow);
>>  err_flow_alloc:
>> -	dst_release(route.tuple[dir].dst);
>> -	dst_release(route.tuple[!dir].dst);
>> +	if (routing) {
>> +		dst_release(route.tuple[dir].dst);
>> +		dst_release(route.tuple[!dir].dst);
>> +	}
>>  err_flow_route:
>>  	clear_bit(IPS_OFFLOAD_BIT, &ct->status);
>>  out:
>> -- 
>> 2.53.0
>>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.