All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port
@ 2026-07-08  9:32 Pablo Neira Ayuso
  2026-07-08  9:32 ` [PATCH nf-next,v1 2/4] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-08  9:32 UTC (permalink / raw)
  To: netfilter-devel

From: Eric Woudstra <ericwouds@gmail.com>

If a port is passed as argument instead of the master, then:

At br_fill_forward_path(): find the master and use it to fill the
forward path.

At br_vlan_fill_forward_path_pvid(): lookup vlan group from port
instead.

Changed call to br_vlan_group() into br_vlan_group_rcu() while at it.

Acked-by: Nikolay Aleksandrov <razor@blackwall.org>
Signed-off-by: Eric Woudstra <ericwouds@gmail.com>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
v1: I took this existing patch for the bridge vlan filtering, but
    bridge vlan filtering is untested at this stage at least for me.

 net/bridge/br_device.c  | 19 ++++++++++++++-----
 net/bridge/br_private.h |  2 ++
 net/bridge/br_vlan.c    |  6 +++++-
 3 files changed, 21 insertions(+), 6 deletions(-)

diff --git a/net/bridge/br_device.c b/net/bridge/br_device.c
index e7f343ab22d3..89f4525a7279 100644
--- a/net/bridge/br_device.c
+++ b/net/bridge/br_device.c
@@ -385,16 +385,25 @@ static int br_del_slave(struct net_device *dev, struct net_device *slave_dev)
 static int br_fill_forward_path(struct net_device_path_ctx *ctx,
 				struct net_device_path *path)
 {
+	struct net_bridge_port *src, *dst;
 	struct net_bridge_fdb_entry *f;
-	struct net_bridge_port *dst;
 	struct net_bridge *br;
 
-	if (netif_is_bridge_port(ctx->dev))
-		return -1;
+	if (netif_is_bridge_port(ctx->dev)) {
+		struct net_device *br_dev;
+
+		br_dev = netdev_master_upper_dev_get_rcu((struct net_device *)ctx->dev);
+		if (!br_dev)
+			return -1;
 
-	br = netdev_priv(ctx->dev);
+		src = br_port_get_rcu(ctx->dev);
+		br = netdev_priv(br_dev);
+	} else {
+		src = NULL;
+		br = netdev_priv(ctx->dev);
+	}
 
-	br_vlan_fill_forward_path_pvid(br, ctx, path);
+	br_vlan_fill_forward_path_pvid(br, src, ctx, path);
 
 	f = br_fdb_find_rcu(br, ctx->daddr, path->bridge.vlan_id);
 	if (!f)
diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h
index d55ea9516e3e..00f5b72ff42d 100644
--- a/net/bridge/br_private.h
+++ b/net/bridge/br_private.h
@@ -1630,6 +1630,7 @@ bool br_vlan_can_enter_range(const struct net_bridge_vlan *v_curr,
 			     const struct net_bridge_vlan *range_end);
 
 void br_vlan_fill_forward_path_pvid(struct net_bridge *br,
+				    struct net_bridge_port *p,
 				    struct net_device_path_ctx *ctx,
 				    struct net_device_path *path);
 int br_vlan_fill_forward_path_mode(struct net_bridge *br,
@@ -1799,6 +1800,7 @@ static inline int nbp_get_num_vlan_infos(struct net_bridge_port *p,
 }
 
 static inline void br_vlan_fill_forward_path_pvid(struct net_bridge *br,
+						  struct net_bridge_port *p,
 						  struct net_device_path_ctx *ctx,
 						  struct net_device_path *path)
 {
diff --git a/net/bridge/br_vlan.c b/net/bridge/br_vlan.c
index 5560afcaaca3..71531499bc73 100644
--- a/net/bridge/br_vlan.c
+++ b/net/bridge/br_vlan.c
@@ -1450,6 +1450,7 @@ int br_vlan_get_pvid_rcu(const struct net_device *dev, u16 *p_pvid)
 EXPORT_SYMBOL_GPL(br_vlan_get_pvid_rcu);
 
 void br_vlan_fill_forward_path_pvid(struct net_bridge *br,
+				    struct net_bridge_port *p,
 				    struct net_device_path_ctx *ctx,
 				    struct net_device_path *path)
 {
@@ -1462,7 +1463,10 @@ void br_vlan_fill_forward_path_pvid(struct net_bridge *br,
 	if (!br_opt_get(br, BROPT_VLAN_ENABLED))
 		return;
 
-	vg = br_vlan_group_rcu(br);
+	if (p)
+		vg = nbp_vlan_group_rcu(p);
+	else
+		vg = br_vlan_group_rcu(br);
 
 	if (idx >= 0 &&
 	    ctx->vlan[idx].proto == br->vlan_proto) {
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH nf-next,v1 2/4] net: pass net_device_path_ctx struct to dev_fill_forward_path()
  2026-07-08  9:32 [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port Pablo Neira Ayuso
@ 2026-07-08  9:32 ` Pablo Neira Ayuso
  2026-07-08  9:32 ` [PATCH nf-next,v1 3/4] net: expose dev_fwd_path() helper via static inline Pablo Neira Ayuso
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 5+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-08  9:32 UTC (permalink / raw)
  To: netfilter-devel

Generalize dev_fill_forward_path() so it can be used by the bridge
family to retrieve the bridge vlan filtering information from the
bridge port when discovering the bridge flowtable path.

Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
v1: A different approach compared to Eric Woudstra's proposal to
    add another function to the net core for the bridge case.
    I am leaning towards making a simple generalization and keep
    the bridge vlan filtering path discovery special case under
    the flowtable at this stage.

 include/linux/netdevice.h          |  2 +-
 net/core/dev.c                     | 18 +++++++-----------
 net/netfilter/nf_flow_table_path.c | 12 +++++++++++-
 3 files changed, 19 insertions(+), 13 deletions(-)

diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 07bf265d0295..41cd092b032c 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -3420,7 +3420,7 @@ void dev_remove_offload(struct packet_offload *po);
 
 int dev_get_iflink(const struct net_device *dev);
 int dev_fill_metadata_dst(struct net_device *dev, struct sk_buff *skb);
-int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr,
+int dev_fill_forward_path(struct net_device_path_ctx *ctx,
 			  struct net_device_path_stack *stack);
 struct net_device *dev_get_by_name(struct net *net, const char *name);
 struct net_device *dev_get_by_name_rcu(struct net *net, const char *name);
diff --git a/net/core/dev.c b/net/core/dev.c
index 4b3d5cfdf6e0..9a065c286d92 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -750,41 +750,37 @@ static struct net_device_path *dev_fwd_path(struct net_device_path_stack *stack)
 	return &stack->path[k];
 }
 
-int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr,
+int dev_fill_forward_path(struct net_device_path_ctx *ctx,
 			  struct net_device_path_stack *stack)
 {
 	const struct net_device *last_dev;
-	struct net_device_path_ctx ctx = {
-		.dev	= dev,
-	};
 	struct net_device_path *path;
 	int ret = 0;
 
-	memcpy(ctx.daddr, daddr, sizeof(ctx.daddr));
 	stack->num_paths = 0;
-	while (ctx.dev && ctx.dev->netdev_ops->ndo_fill_forward_path) {
-		last_dev = ctx.dev;
+	while (ctx->dev && ctx->dev->netdev_ops->ndo_fill_forward_path) {
+		last_dev = ctx->dev;
 		path = dev_fwd_path(stack);
 		if (!path)
 			return -1;
 
 		memset(path, 0, sizeof(struct net_device_path));
-		ret = ctx.dev->netdev_ops->ndo_fill_forward_path(&ctx, path);
+		ret = ctx->dev->netdev_ops->ndo_fill_forward_path(ctx, path);
 		if (ret < 0)
 			return -1;
 
-		if (WARN_ON_ONCE(last_dev == ctx.dev))
+		if (WARN_ON_ONCE(last_dev == ctx->dev))
 			return -1;
 	}
 
-	if (!ctx.dev)
+	if (!ctx->dev)
 		return ret;
 
 	path = dev_fwd_path(stack);
 	if (!path)
 		return -1;
 	path->type = DEV_PATH_ETHERNET;
-	path->dev = ctx.dev;
+	path->dev = ctx->dev;
 
 	return ret;
 }
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index 98c03b487f52..007e9781902a 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -42,6 +42,14 @@ static bool nft_is_valid_ether_device(const struct net_device *dev)
 	return true;
 }
 
+static void nft_dev_fill_forward_path_init(struct net_device_path_ctx *ctx,
+					   const struct net_device *dev, const u8 *daddr)
+{
+	memset(ctx, 0, sizeof(*ctx));
+	ctx->dev	= dev;
+	memcpy(ctx->daddr, daddr, sizeof(ctx->daddr));
+}
+
 static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
 				     const struct dst_entry *dst_cache,
 				     const struct nf_conn *ct,
@@ -50,6 +58,7 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
 {
 	const void *daddr = &ct->tuplehash[!dir].tuple.src.u3;
 	struct net_device *dev = dst_cache->dev;
+	struct net_device_path_ctx ctx;
 	struct neighbour *n;
 	u8 nud_state;
 
@@ -71,8 +80,9 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
 	if (!(nud_state & NUD_VALID))
 		return -1;
 
+	nft_dev_fill_forward_path_init(&ctx, dev, ha);
 out:
-	return dev_fill_forward_path(dev, ha, stack);
+	return dev_fill_forward_path(&ctx, stack);
 }
 
 struct nft_forward_info {
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH nf-next,v1 3/4] net: expose dev_fwd_path() helper via static inline
  2026-07-08  9:32 [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port Pablo Neira Ayuso
  2026-07-08  9:32 ` [PATCH nf-next,v1 2/4] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
@ 2026-07-08  9:32 ` Pablo Neira Ayuso
  2026-07-08  9:32 ` [PATCH nf-next,v1 4/4] netfilter: flowtable: initial bridge support Pablo Neira Ayuso
  2026-07-12  9:56 ` [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port Eric Woudstra
  3 siblings, 0 replies; 5+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-08  9:32 UTC (permalink / raw)
  To: netfilter-devel

This is a preparation patch to be used by the bridge family to retrieve
the bridge vlan filtering information from the bridge port when
discovering the bridge flowtable path.

Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
v1: Just a preparation patch to allow to move the bridge vlan filtering
    path discovery code to the flowtable infrastructure.

 include/linux/netdevice.h | 11 +++++++++++
 net/core/dev.c            | 10 ----------
 2 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 41cd092b032c..9393d3088042 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -3420,6 +3420,17 @@ void dev_remove_offload(struct packet_offload *po);
 
 int dev_get_iflink(const struct net_device *dev);
 int dev_fill_metadata_dst(struct net_device *dev, struct sk_buff *skb);
+
+static inline struct net_device_path *dev_fwd_path(struct net_device_path_stack *stack)
+{
+	int k = stack->num_paths++;
+
+	if (k >= NET_DEVICE_PATH_STACK_MAX)
+		return NULL;
+
+	return &stack->path[k];
+}
+
 int dev_fill_forward_path(struct net_device_path_ctx *ctx,
 			  struct net_device_path_stack *stack);
 struct net_device *dev_get_by_name(struct net *net, const char *name);
diff --git a/net/core/dev.c b/net/core/dev.c
index 9a065c286d92..6b0e83fe743f 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -740,16 +740,6 @@ int dev_fill_metadata_dst(struct net_device *dev, struct sk_buff *skb)
 }
 EXPORT_SYMBOL_GPL(dev_fill_metadata_dst);
 
-static struct net_device_path *dev_fwd_path(struct net_device_path_stack *stack)
-{
-	int k = stack->num_paths++;
-
-	if (k >= NET_DEVICE_PATH_STACK_MAX)
-		return NULL;
-
-	return &stack->path[k];
-}
-
 int dev_fill_forward_path(struct net_device_path_ctx *ctx,
 			  struct net_device_path_stack *stack)
 {
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH nf-next,v1 4/4] netfilter: flowtable: initial bridge support
  2026-07-08  9:32 [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port Pablo Neira Ayuso
  2026-07-08  9:32 ` [PATCH nf-next,v1 2/4] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
  2026-07-08  9:32 ` [PATCH nf-next,v1 3/4] net: expose dev_fwd_path() helper via static inline Pablo Neira Ayuso
@ 2026-07-08  9:32 ` Pablo Neira Ayuso
  2026-07-12  9:56 ` [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port Eric Woudstra
  3 siblings, 0 replies; 5+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-08  9:32 UTC (permalink / raw)
  To: netfilter-devel

This patch adds bridge flowtable support, this allows to define a
shortcut between two bridge ports. This is complementary to the
existing inet family flowtable support.

Set up does not require userspace updates, an example ruleset to
enable the flowtable in the bridge family is provided here below:

 table bridge x {
        flowtable y {
                hook ingress priority 0
                devices = { veth0, veth1 }
        }
        chain forward {
                type filter hook forward priority 0
                ip protocol tcp flow add @y counter
                counter
        }
 }

I decided to add an explicit nft_flow_offload_bridge_eval() instead of
recycling the existing inet function by adding branches to skip the
routing part which is obviously not needed in the bridge path. I
consider this mostly boiler plate for feature extensibility and better
maintability is better to keep it separated. Similarly, the bridge hook
that represents the flowtable bridge datapath is implemented in a
separated function.

Although connection tracking in the bridge does not support the tracking
of IP flows encapsulated in PPPoE and VLAN tracking yet, there are
scenarios that involved PPPoE and VLAN that can be supported already,
such as those where packets flows through the bridge with no tagging,
eg. a VLAN device is used as a bridge port which decapsulates the
packets at the ingress path.

Tested with:
- Plain forwarding between bridge ports with no VLAN tagging.
- VLAN device used in bridged ports.

The bridge vlan filtering supported is untested at this stage based on
previous work by Eric Woudstra.

Hardware offload is disabled until there is a driver supporting this.

Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
v1: Different approach to what Eric Woudstra proposes:
    - Instead of extending the existing _eval() and _hook() function
      that represent the flowtable offload stage and datapath, add new
      specific functions. It is more code, but it is boilerplate code
      and it might help to make it more maintainable as new specific
      bridge features are added, I believe.
    - As noted in the commit description, bridge vlan filtering support
      is untested, I have used the existing code posted by Eric Woudstra
      in this regard.
   
 include/net/netfilter/nf_flow_table.h |   7 ++
 net/netfilter/nf_flow_table_inet.c    |  12 +++
 net/netfilter/nf_flow_table_ip.c      | 134 ++++++++++++++++++++++++++
 net/netfilter/nf_flow_table_offload.c |   8 +-
 net/netfilter/nf_flow_table_path.c    |  95 ++++++++++++++++++
 net/netfilter/nft_flow_offload.c      |  88 ++++++++++++++++-
 6 files changed, 340 insertions(+), 4 deletions(-)

diff --git a/include/net/netfilter/nf_flow_table.h b/include/net/netfilter/nf_flow_table.h
index ce414118962f..6e76a6879ff3 100644
--- a/include/net/netfilter/nf_flow_table.h
+++ b/include/net/netfilter/nf_flow_table.h
@@ -248,6 +248,8 @@ struct nft_pktinfo;
 int nft_flow_route(const struct nft_pktinfo *pkt, const struct nf_conn *ct,
 		   struct nf_flow_route *route, enum ip_conntrack_dir dir,
 		   struct nft_flowtable *ft);
+int nft_flow_bridge(struct flow_offload *flow, const struct nft_pktinfo *pkt,
+		    enum ip_conntrack_dir dir, struct nft_flowtable *ft);
 
 static inline int
 nf_flow_table_offload_add_cb(struct nf_flowtable *flow_table,
@@ -342,6 +344,8 @@ unsigned int nf_flow_offload_ip_hook(void *priv, struct sk_buff *skb,
 				     const struct nf_hook_state *state);
 unsigned int nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
 				       const struct nf_hook_state *state);
+unsigned int nf_flow_offload_bridge_hook(void *priv, struct sk_buff *skb,
+					 const struct nf_hook_state *state);
 
 #if (IS_BUILTIN(CONFIG_NF_FLOW_TABLE) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)) || \
     (IS_MODULE(CONFIG_NF_FLOW_TABLE) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES))
@@ -377,6 +381,9 @@ int nf_flow_rule_route_ipv4(struct net *net, struct flow_offload *flow,
 int nf_flow_rule_route_ipv6(struct net *net, struct flow_offload *flow,
 			    enum flow_offload_tuple_dir dir,
 			    struct nf_flow_rule *flow_rule);
+int nf_flow_rule_bridge(struct net *net, struct flow_offload *flow,
+			enum flow_offload_tuple_dir dir,
+			struct nf_flow_rule *flow_rule);
 
 int nf_flow_table_offload_init(void);
 void nf_flow_table_offload_exit(void);
diff --git a/net/netfilter/nf_flow_table_inet.c b/net/netfilter/nf_flow_table_inet.c
index b0f199171932..44790a0d3012 100644
--- a/net/netfilter/nf_flow_table_inet.c
+++ b/net/netfilter/nf_flow_table_inet.c
@@ -65,6 +65,15 @@ static int nf_flow_rule_route_inet(struct net *net,
 	return err;
 }
 
+static struct nf_flowtable_type flowtable_bridge = {
+	.family		= NFPROTO_BRIDGE,
+	.init		= nf_flow_table_init,
+	.setup		= nf_flow_table_offload_setup,
+	.free		= nf_flow_table_free,
+	.hook		= nf_flow_offload_bridge_hook,
+	.owner		= THIS_MODULE,
+};
+
 static struct nf_flowtable_type flowtable_inet = {
 	.family		= NFPROTO_INET,
 	.init		= nf_flow_table_init,
@@ -97,6 +106,7 @@ static struct nf_flowtable_type flowtable_ipv6 = {
 
 static int __init nf_flow_inet_module_init(void)
 {
+	nft_register_flowtable_type(&flowtable_bridge);
 	nft_register_flowtable_type(&flowtable_ipv4);
 	nft_register_flowtable_type(&flowtable_ipv6);
 	nft_register_flowtable_type(&flowtable_inet);
@@ -109,6 +119,7 @@ static void __exit nf_flow_inet_module_exit(void)
 	nft_unregister_flowtable_type(&flowtable_inet);
 	nft_unregister_flowtable_type(&flowtable_ipv6);
 	nft_unregister_flowtable_type(&flowtable_ipv4);
+	nft_unregister_flowtable_type(&flowtable_bridge);
 }
 
 module_init(nf_flow_inet_module_init);
@@ -118,5 +129,6 @@ MODULE_LICENSE("GPL");
 MODULE_AUTHOR("Pablo Neira Ayuso <pablo@netfilter.org>");
 MODULE_ALIAS_NF_FLOWTABLE(AF_INET);
 MODULE_ALIAS_NF_FLOWTABLE(AF_INET6);
+MODULE_ALIAS_NF_FLOWTABLE(AF_BRIDGE);
 MODULE_ALIAS_NF_FLOWTABLE(1); /* NFPROTO_INET */
 MODULE_DESCRIPTION("Netfilter flow table mixed IPv4/IPv6 module");
diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c
index 44f4d74f2982..ce4998ac44fe 100644
--- a/net/netfilter/nf_flow_table_ip.c
+++ b/net/netfilter/nf_flow_table_ip.c
@@ -1172,3 +1172,137 @@ nf_flow_offload_ipv6_hook(void *priv, struct sk_buff *skb,
 	return nf_flow_queue_xmit(state->net, skb, &xmit);
 }
 EXPORT_SYMBOL_GPL(nf_flow_offload_ipv6_hook);
+
+static int nf_flow_bridge_xmit(struct net *net,
+			       struct nf_flowtable *flow_table,
+			       struct flow_offload *flow,
+			       enum flow_offload_tuple_dir dir,
+			       struct sk_buff *skb)
+{
+	struct flow_offload_tuple *other_tuple = &flow->tuplehash[!dir].tuple;
+	struct flow_offload_tuple *this_tuple = &flow->tuplehash[dir].tuple;
+	struct nf_flow_xmit xmit = {};
+
+	xmit.outdev = dev_get_by_index_rcu(net, this_tuple->out.ifidx);
+	if (!xmit.outdev) {
+		flow_offload_teardown(flow);
+		return NF_DROP;
+	}
+
+	if (flow_table->flags & NF_FLOWTABLE_COUNTER)
+		nf_ct_acct_update(flow->ct, dir, skb->len);
+
+	xmit.dest = this_tuple->out.h_dest;
+	xmit.source = this_tuple->out.h_source;
+	xmit.tuple = other_tuple;
+	xmit.needs_gso_segment = this_tuple->needs_gso_segment;
+
+	return nf_flow_queue_xmit(net, skb, &xmit);
+}
+
+static unsigned int
+nf_flow_offload_ip_bridge(void *priv, struct sk_buff *skb,
+			  const struct nf_hook_state *state)
+{
+	struct flow_offload_tuple_rhash *tuplehash;
+	struct nf_flowtable *flow_table = priv;
+	enum flow_offload_tuple_dir dir;
+	struct nf_flowtable_ctx ctx = {
+		.in	= state->in,
+	};
+	struct flow_offload *flow;
+	unsigned int thoff;
+	struct iphdr *iph;
+
+	tuplehash = nf_flow_offload_lookup(&ctx, flow_table, skb);
+	if (!tuplehash)
+		return NF_ACCEPT;
+
+	dir = tuplehash->tuple.dir;
+	flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
+
+	iph = (struct iphdr *)(skb_network_header(skb) + ctx.offset);
+	thoff = (iph->ihl * 4) + ctx.offset;
+	if (nf_flow_state_check(flow, iph->protocol, skb, thoff))
+		return NF_ACCEPT;
+
+	if (skb_ensure_writable(skb, thoff + ctx.hdrsize))
+		return NF_DROP;
+
+	flow_offload_refresh(flow_table, flow, false);
+	nf_flow_encap_pop(&ctx, skb, tuplehash);
+	skb_clear_tstamp(skb);
+
+	return nf_flow_bridge_xmit(state->net, flow_table, flow, dir, skb);
+}
+
+static unsigned int
+nf_flow_offload_ipv6_bridge(void *priv, struct sk_buff *skb,
+			    const struct nf_hook_state *state)
+{
+	struct flow_offload_tuple_rhash *tuplehash;
+	struct nf_flowtable *flow_table = priv;
+	enum flow_offload_tuple_dir dir;
+	struct nf_flowtable_ctx ctx = {
+		.in	= state->in,
+	};
+	struct flow_offload *flow;
+	struct ipv6hdr *ip6h;
+	unsigned int thoff;
+
+	tuplehash = nf_flow_offload_ipv6_lookup(&ctx, flow_table, skb);
+	if (!tuplehash)
+		return NF_ACCEPT;
+
+	dir = tuplehash->tuple.dir;
+	flow = container_of(tuplehash, struct flow_offload, tuplehash[dir]);
+
+	ip6h = (struct ipv6hdr *)(skb_network_header(skb) + ctx.offset);
+	thoff = sizeof(*ip6h) + ctx.offset;
+	if (nf_flow_state_check(flow, ip6h->nexthdr, skb, thoff))
+		return NF_ACCEPT;
+
+	if (skb_ensure_writable(skb, thoff + ctx.hdrsize))
+		return NF_DROP;
+
+	flow_offload_refresh(flow_table, flow, false);
+	nf_flow_encap_pop(&ctx, skb, tuplehash);
+	skb_clear_tstamp(skb);
+
+	return nf_flow_bridge_xmit(state->net, flow_table, flow, dir, skb);
+}
+
+unsigned int
+nf_flow_offload_bridge_hook(void *priv, struct sk_buff *skb,
+			    const struct nf_hook_state *state)
+{
+	struct vlan_ethhdr *veth;
+	__be16 proto;
+
+	switch (skb->protocol) {
+	case htons(ETH_P_8021Q):
+		if (!pskb_may_pull(skb, skb_mac_offset(skb) + sizeof(*veth)))
+			return NF_ACCEPT;
+
+		veth = (struct vlan_ethhdr *)skb_mac_header(skb);
+		proto = veth->h_vlan_encapsulated_proto;
+		break;
+	case htons(ETH_P_PPP_SES):
+		if (!nf_flow_pppoe_proto(skb, &proto))
+			return NF_ACCEPT;
+		break;
+	default:
+		proto = skb->protocol;
+		break;
+	}
+
+	switch (proto) {
+	case htons(ETH_P_IP):
+		return nf_flow_offload_ip_bridge(priv, skb, state);
+	case htons(ETH_P_IPV6):
+		return nf_flow_offload_ipv6_bridge(priv, skb, state);
+	}
+
+	return NF_ACCEPT;
+}
+EXPORT_SYMBOL_GPL(nf_flow_offload_bridge_hook);
diff --git a/net/netfilter/nf_flow_table_offload.c b/net/netfilter/nf_flow_table_offload.c
index d26874f1b15f..81c6313f660c 100644
--- a/net/netfilter/nf_flow_table_offload.c
+++ b/net/netfilter/nf_flow_table_offload.c
@@ -1101,9 +1101,11 @@ nf_flow_offload_work_alloc(struct nf_flowtable *flowtable,
 	return offload;
 }
 
-static bool nf_flow_offload_unsupported(struct flow_offload *flow)
+static bool nf_flow_offload_unsupported(struct nf_flowtable *flowtable,
+					struct flow_offload *flow)
 {
-	if (flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.tun_num ||
+	if (flowtable->type->family == NFPROTO_BRIDGE ||
+	    flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.tun_num ||
 	    flow->tuplehash[FLOW_OFFLOAD_DIR_REPLY].tuple.tun_num)
 		return true;
 
@@ -1125,7 +1127,7 @@ void nf_flow_offload_refresh(struct nf_flowtable *flowtable,
 void nf_flow_offload_add(struct nf_flowtable *flowtable,
 			 struct flow_offload *flow)
 {
-	if (nf_flow_offload_unsupported(flow))
+	if (nf_flow_offload_unsupported(flowtable, flow))
 		return;
 
 	__set_bit(NF_FLOW_HW, &flow->flags);
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index 007e9781902a..119eecd4b1db 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -8,6 +8,7 @@
 #include <linux/spinlock.h>
 #include <linux/netfilter/nf_conntrack_common.h>
 #include <linux/netfilter/nf_tables.h>
+#include <linux/if_vlan.h>
 #include <net/ip.h>
 #include <net/inet_dscp.h>
 #include <net/netfilter/nf_tables.h>
@@ -360,3 +361,97 @@ 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_br_vlan_path(struct net_device_path_ctx *ctx,
+				     struct net_device_path_stack *stack,
+				     const struct net_device *dev)
+{
+	const struct net_device *br_dev;
+	struct net_device_path *path;
+
+	if (!netif_is_bridge_port(dev))
+		return -1;
+
+	br_dev = netdev_master_upper_dev_get_rcu((struct net_device *)dev);
+	if (!br_dev || !br_dev->netdev_ops->ndo_fill_forward_path)
+		return -1;
+
+	path = dev_fwd_path(stack);
+	if (!path)
+		return -1;
+
+	memset(path, 0, sizeof(struct net_device_path));
+	ctx->dev = dev;
+
+	if (br_dev->netdev_ops->ndo_fill_forward_path(ctx, path) < 0)
+		return -1;
+
+	return 0;
+}
+
+static int nft_dev_fill_bridge_path(struct flow_offload *flow,
+				    struct nft_flowtable *ft,
+				    enum ip_conntrack_dir dir,
+				    const struct net_device *dev,
+				    unsigned char *src_ha,
+				    unsigned char *dst_ha)
+{
+	struct flow_offload_tuple *this_tuple = &flow->tuplehash[dir].tuple;
+	struct net_device_path_stack stack;
+	struct nft_forward_info info = {};
+	struct net_device_path_ctx ctx;
+	int i, j = 0;
+
+	nft_dev_fill_forward_path_init(&ctx, dev, dst_ha);
+
+	if (dev_fill_forward_path(&ctx, &stack) < 0 ||
+	    nft_dev_fill_br_vlan_path(&ctx, &stack, dev) < 0 ||
+	    nft_dev_path_info(&stack, &info, dst_ha, &ft->data) < 0)
+		return -1;
+
+	if (!nft_flowtable_find_dev(info.indev, ft))
+		return -1;
+
+	this_tuple->iifidx = info.indev->ifindex;
+	for (i = info.num_encaps - 1; i >= 0; i--) {
+		this_tuple->encap[j].id = info.encap[i].id;
+		this_tuple->encap[j].proto = info.encap[i].proto;
+		if (info.ingress_vlans & BIT(i))
+			this_tuple->in_vlan_ingress |= BIT(j);
+		j++;
+	}
+	this_tuple->encap_num = info.num_encaps;
+
+	ether_addr_copy(this_tuple->out.h_source, src_ha);
+	ether_addr_copy(this_tuple->out.h_dest, dst_ha);
+	this_tuple->xmit_type = FLOW_OFFLOAD_XMIT_DIRECT;
+
+	return 0;
+}
+
+int nft_flow_bridge(struct flow_offload *flow, const struct nft_pktinfo *pkt,
+		    enum ip_conntrack_dir dir, struct nft_flowtable *ft)
+{
+	struct flow_offload_tuple *other_tuple = &flow->tuplehash[!dir].tuple;
+	struct flow_offload_tuple *this_tuple = &flow->tuplehash[dir].tuple;
+	const struct net_device *outdev = nft_out(pkt);
+	const struct net_device *indev = nft_in(pkt);
+	struct ethhdr *eth = eth_hdr(pkt->skb);
+	int err;
+
+	err = nft_dev_fill_bridge_path(flow, ft, dir, indev,
+				       eth->h_source, eth->h_dest);
+	if (err < 0)
+		return err;
+
+	err = nft_dev_fill_bridge_path(flow, ft, !dir, outdev,
+				       eth->h_dest, eth->h_source);
+	if (err < 0)
+		return err;
+
+	this_tuple->out.ifidx = other_tuple->iifidx;
+	other_tuple->out.ifidx = this_tuple->iifidx;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(nft_flow_bridge);
diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c
index 32b4281038dd..d1f145a401d1 100644
--- a/net/netfilter/nft_flow_offload.c
+++ b/net/netfilter/nft_flow_offload.c
@@ -135,6 +135,64 @@ static void nft_flow_offload_eval(const struct nft_expr *expr,
 	regs->verdict.code = NFT_BREAK;
 }
 
+static void nft_flow_offload_bridge_eval(const struct nft_expr *expr,
+					 struct nft_regs *regs,
+					 const struct nft_pktinfo *pkt)
+{
+	struct nft_flow_offload *priv = nft_expr_priv(expr);
+	struct nf_flowtable *flowtable = &priv->flowtable->data;
+	struct tcphdr _tcph, *tcph = NULL;
+	enum ip_conntrack_info ctinfo;
+	struct flow_offload *flow;
+	enum ip_conntrack_dir dir;
+	struct nf_conn *ct;
+	int ret;
+
+	/* Is this an IP packet? If not, skip. */
+	if (!pkt->flags)
+		goto out;
+
+	ct = nf_ct_get(pkt->skb, &ctinfo);
+	if (!ct || !nf_ct_is_confirmed(ct))
+		goto out;
+
+	switch (ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.dst.protonum) {
+	case IPPROTO_TCP:
+		tcph = skb_header_pointer(pkt->skb, nft_thoff(pkt),
+					  sizeof(_tcph), &_tcph);
+		if (unlikely(!tcph || tcph->fin || tcph->rst ||
+			     !nf_conntrack_tcp_established(ct)))
+			goto out;
+		break;
+	case IPPROTO_UDP:
+		break;
+	}
+
+	if (test_and_set_bit(IPS_OFFLOAD_BIT, &ct->status))
+		goto out;
+
+	flow = flow_offload_alloc(ct);
+	if (!flow)
+		goto err_flow_forward;
+
+	dir = CTINFO2DIR(ctinfo);
+	if (nft_flow_bridge(flow, pkt, dir, priv->flowtable) < 0)
+		goto err_flow_add;
+
+	ret = flow_offload_add(flowtable, flow);
+	if (ret < 0)
+		goto err_flow_add;
+
+	return;
+
+err_flow_add:
+	flow_offload_free(flow);
+err_flow_forward:
+	clear_bit(IPS_OFFLOAD_BIT, &ct->status);
+out:
+	regs->verdict.code = NFT_BREAK;
+}
+
 static int nft_flow_offload_validate(const struct nft_ctx *ctx,
 				     const struct nft_expr *expr)
 {
@@ -142,7 +200,8 @@ static int nft_flow_offload_validate(const struct nft_ctx *ctx,
 
 	if (ctx->family != NFPROTO_IPV4 &&
 	    ctx->family != NFPROTO_IPV6 &&
-	    ctx->family != NFPROTO_INET)
+	    ctx->family != NFPROTO_INET &&
+	    ctx->family != NFPROTO_BRIDGE)
 		return -EOPNOTSUPP;
 
 	return nft_chain_validate_hooks(ctx->chain, hook_mask);
@@ -235,6 +294,27 @@ static struct nft_expr_type nft_flow_offload_type __read_mostly = {
 	.owner		= THIS_MODULE,
 };
 
+static const struct nft_expr_ops nft_flow_offload_bridge_ops = {
+	.type		= &nft_flow_offload_type,
+	.size		= NFT_EXPR_SIZE(sizeof(struct nft_flow_offload)),
+	.eval		= nft_flow_offload_bridge_eval,
+	.init		= nft_flow_offload_init,
+	.activate	= nft_flow_offload_activate,
+	.deactivate	= nft_flow_offload_deactivate,
+	.destroy	= nft_flow_offload_destroy,
+	.validate	= nft_flow_offload_validate,
+	.dump		= nft_flow_offload_dump,
+};
+
+static struct nft_expr_type nft_flow_offload_bridge_type __read_mostly = {
+	.name		= "flow_offload",
+	.family		= NFPROTO_BRIDGE,
+	.ops		= &nft_flow_offload_bridge_ops,
+	.policy		= nft_flow_offload_policy,
+	.maxattr	= NFTA_FLOW_MAX,
+	.owner		= THIS_MODULE,
+};
+
 static int flow_offload_netdev_event(struct notifier_block *this,
 				     unsigned long event, void *ptr)
 {
@@ -264,8 +344,14 @@ static int __init nft_flow_offload_module_init(void)
 	if (err < 0)
 		goto register_expr;
 
+	err = nft_register_expr(&nft_flow_offload_bridge_type);
+	if (err < 0)
+		goto register_bridge_expr;
+
 	return 0;
 
+register_bridge_expr:
+	nft_unregister_expr(&nft_flow_offload_type);
 register_expr:
 	unregister_netdevice_notifier(&flow_offload_netdev_notifier);
 err:
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port
  2026-07-08  9:32 [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port Pablo Neira Ayuso
                   ` (2 preceding siblings ...)
  2026-07-08  9:32 ` [PATCH nf-next,v1 4/4] netfilter: flowtable: initial bridge support Pablo Neira Ayuso
@ 2026-07-12  9:56 ` Eric Woudstra
  3 siblings, 0 replies; 5+ messages in thread
From: Eric Woudstra @ 2026-07-12  9:56 UTC (permalink / raw)
  To: Pablo Neira Ayuso, netfilter-devel; +Cc: Nikolay Aleksandrov, Florian Westphal



On 7/8/26 11:32 AM, Pablo Neira Ayuso wrote:
> From: Eric Woudstra <ericwouds@gmail.com>
> 
> If a port is passed as argument instead of the master, then:
> 
> At br_fill_forward_path(): find the master and use it to fill the
> forward path.
> 
> At br_vlan_fill_forward_path_pvid(): lookup vlan group from port
> instead.
> 
> Changed call to br_vlan_group() into br_vlan_group_rcu() while at it.
> 
> Acked-by: Nikolay Aleksandrov <razor@blackwall.org>
> Signed-off-by: Eric Woudstra <ericwouds@gmail.com>
> Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
> ---
> v1: I took this existing patch for the bridge vlan filtering, but
>     bridge vlan filtering is untested at this stage at least for me.
> 
>  net/bridge/br_device.c  | 19 ++++++++++++++-----
>  net/bridge/br_private.h |  2 ++
>  net/bridge/br_vlan.c    |  6 +++++-
>  3 files changed, 21 insertions(+), 6 deletions(-)
> 
> diff --git a/net/bridge/br_device.c b/net/bridge/br_device.c
> index e7f343ab22d3..89f4525a7279 100644
> --- a/net/bridge/br_device.c
> +++ b/net/bridge/br_device.c
> @@ -385,16 +385,25 @@ static int br_del_slave(struct net_device *dev, struct net_device *slave_dev)
>  static int br_fill_forward_path(struct net_device_path_ctx *ctx,
>  				struct net_device_path *path)
>  {
> +	struct net_bridge_port *src, *dst;
>  	struct net_bridge_fdb_entry *f;
> -	struct net_bridge_port *dst;
>  	struct net_bridge *br;
>  
> -	if (netif_is_bridge_port(ctx->dev))
> -		return -1;
> +	if (netif_is_bridge_port(ctx->dev)) {
> +		struct net_device *br_dev;
> +
> +		br_dev = netdev_master_upper_dev_get_rcu((struct net_device *)ctx->dev);
> +		if (!br_dev)
> +			return -1;
>  
> -	br = netdev_priv(ctx->dev);
> +		src = br_port_get_rcu(ctx->dev);
> +		br = netdev_priv(br_dev);
> +	} else {

So as per Nikolay's comment on another thread, Can add here:

		if (!netif_is_bridge_master(ctx->dev))
			return -1;

So that it can continue normally if ctx->dev is a bridge master.

> +		src = NULL;
> +		br = netdev_priv(ctx->dev);
> +	}
>  
> -	br_vlan_fill_forward_path_pvid(br, ctx, path);
> +	br_vlan_fill_forward_path_pvid(br, src, ctx, path);
>  
>  	f = br_fdb_find_rcu(br, ctx->daddr, path->bridge.vlan_id);
>  	if (!f)
> diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h
> index d55ea9516e3e..00f5b72ff42d 100644
> --- a/net/bridge/br_private.h
> +++ b/net/bridge/br_private.h
> @@ -1630,6 +1630,7 @@ bool br_vlan_can_enter_range(const struct net_bridge_vlan *v_curr,
>  			     const struct net_bridge_vlan *range_end);
>  
>  void br_vlan_fill_forward_path_pvid(struct net_bridge *br,
> +				    struct net_bridge_port *p,
>  				    struct net_device_path_ctx *ctx,
>  				    struct net_device_path *path);
>  int br_vlan_fill_forward_path_mode(struct net_bridge *br,
> @@ -1799,6 +1800,7 @@ static inline int nbp_get_num_vlan_infos(struct net_bridge_port *p,
>  }
>  
>  static inline void br_vlan_fill_forward_path_pvid(struct net_bridge *br,
> +						  struct net_bridge_port *p,
>  						  struct net_device_path_ctx *ctx,
>  						  struct net_device_path *path)
>  {
> diff --git a/net/bridge/br_vlan.c b/net/bridge/br_vlan.c
> index 5560afcaaca3..71531499bc73 100644
> --- a/net/bridge/br_vlan.c
> +++ b/net/bridge/br_vlan.c
> @@ -1450,6 +1450,7 @@ int br_vlan_get_pvid_rcu(const struct net_device *dev, u16 *p_pvid)
>  EXPORT_SYMBOL_GPL(br_vlan_get_pvid_rcu);
>  
>  void br_vlan_fill_forward_path_pvid(struct net_bridge *br,
> +				    struct net_bridge_port *p,
>  				    struct net_device_path_ctx *ctx,
>  				    struct net_device_path *path)
>  {
> @@ -1462,7 +1463,10 @@ void br_vlan_fill_forward_path_pvid(struct net_bridge *br,
>  	if (!br_opt_get(br, BROPT_VLAN_ENABLED))
>  		return;
>  
> -	vg = br_vlan_group_rcu(br);
> +	if (p)
> +		vg = nbp_vlan_group_rcu(p);
> +	else
> +		vg = br_vlan_group_rcu(br);
>  
>  	if (idx >= 0 &&
>  	    ctx->vlan[idx].proto == br->vlan_proto) {


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-07-12  9:56 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-08  9:32 [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port Pablo Neira Ayuso
2026-07-08  9:32 ` [PATCH nf-next,v1 2/4] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
2026-07-08  9:32 ` [PATCH nf-next,v1 3/4] net: expose dev_fwd_path() helper via static inline Pablo Neira Ayuso
2026-07-08  9:32 ` [PATCH nf-next,v1 4/4] netfilter: flowtable: initial bridge support Pablo Neira Ayuso
2026-07-12  9:56 ` [PATCH nf-next,v1 1/4] bridge: Add filling forward path from port to port Eric Woudstra

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.