* [PATCH 1/5] net: export __dev_fill_forward_path
2026-06-29 12:32 [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload Daniel Pawlik
@ 2026-06-29 12:32 ` Daniel Pawlik
2026-06-29 12:32 ` [PATCH 2/5] net: bridge: add flow offload helpers Daniel Pawlik
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Daniel Pawlik @ 2026-06-29 12:32 UTC (permalink / raw)
To: netfilter-devel, netdev
Cc: pablo, fw, phil, davem, edumazet, kuba, pabeni, horms,
andrew+netdev, razor, idosch, matthias.bgg,
angelogioacchino.delregno, bridge, coreteam, linux-mediatek,
linux-arm-kernel, rchen14b, lorenzo, Daniel Pawlik
From: Ryan Chen <rchen14b@gmail.com>
Export __dev_fill_forward_path() which accepts a caller-supplied
net_device_path_ctx, allowing callers to pre-populate context (e.g.
VLAN state) before the forward path walk. The existing
dev_fill_forward_path() is refactored to call it.
This is a prerequisite for nft_flow_offload bridge offload, which needs
to supply a pre-populated ctx for bridge port devices.
Signed-off-by: Ryan Chen <rchen14b@gmail.com>
Signed-off-by: Daniel Pawlik <pawlik.dan@gmail.com>
---
include/linux/netdevice.h | 2 ++
net/core/dev.c | 32 ++++++++++++++++++++------------
2 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 9981d637f8b5..c1d0b897de95 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -3422,6 +3422,8 @@ 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,
struct net_device_path_stack *stack);
+int __dev_fill_forward_path(struct net_device_path_ctx *ctx, const u8 *daddr,
+ 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);
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 4b3d5cfdf6e0..62f1d0b64c76 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -750,44 +750,52 @@ 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,
- struct net_device_path_stack *stack)
+int __dev_fill_forward_path(struct net_device_path_ctx *ctx, const u8 *daddr,
+ 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));
+ 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;
}
+EXPORT_SYMBOL_GPL(__dev_fill_forward_path);
+
+int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr,
+ struct net_device_path_stack *stack)
+{
+ struct net_device_path_ctx ctx = {
+ .dev = dev,
+ };
+
+ return __dev_fill_forward_path(&ctx, daddr, stack);
+}
EXPORT_SYMBOL_GPL(dev_fill_forward_path);
/* must be called under rcu_read_lock(), as we dont take a reference */
--
2.54.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 2/5] net: bridge: add flow offload helpers
2026-06-29 12:32 [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload Daniel Pawlik
2026-06-29 12:32 ` [PATCH 1/5] net: export __dev_fill_forward_path Daniel Pawlik
@ 2026-06-29 12:32 ` Daniel Pawlik
2026-06-29 12:32 ` [PATCH 3/5] netfilter: nf_flow_table_path: add L2 bridge offload Daniel Pawlik
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Daniel Pawlik @ 2026-06-29 12:32 UTC (permalink / raw)
To: netfilter-devel, netdev
Cc: pablo, fw, phil, davem, edumazet, kuba, pabeni, horms,
andrew+netdev, razor, idosch, matthias.bgg,
angelogioacchino.delregno, bridge, coreteam, linux-mediatek,
linux-arm-kernel, rchen14b, lorenzo, Daniel Pawlik
Add three helpers that expose the bridge state needed by nft_flow_offload
without requiring callers to include net/bridge/br_private.h. Each
performs a single br_port_get_rcu() lookup:
- br_fdb_has_forwarding_entry_rcu(): resolves the VLAN id for the packet
(skb tag or PVID when filtering is on, 0 otherwise) then checks whether
the bridge FDB contains a forwarding entry (dst != NULL, non-local) for
the resulting MAC/VLAN pair.
- br_vlan_get_offload_info_rcu(): when VLAN filtering is active, returns
the VLAN id (skb tag or PVID) and writes the bridge VLAN protocol to
*proto in a single port lookup. Returns 0 when filtering is off.
- br_vlan_is_enabled_rcu(): returns true when VLAN filtering is enabled
on the bridge a port device belongs to.
Based on MediaTek SDK patches by Bo-Cun Chen <bc-bocun.chen@mediatek.com>
and the OpenWrt bridge offload series by Ryan Chen <rchen14b@gmail.com>.
Signed-off-by: Daniel Pawlik <pawlik.dan@gmail.com>
---
include/linux/if_bridge.h | 23 ++++++++++++++++++++
net/bridge/br_fdb.c | 32 ++++++++++++++++++++++++++++
net/bridge/br_vlan.c | 45 +++++++++++++++++++++++++++++++++++++++
3 files changed, 100 insertions(+)
diff --git a/include/linux/if_bridge.h b/include/linux/if_bridge.h
index 75673b8bffcb..c1cae54749c5 100644
--- a/include/linux/if_bridge.h
+++ b/include/linux/if_bridge.h
@@ -148,6 +148,9 @@ int br_vlan_get_info(const struct net_device *dev, u16 vid,
struct bridge_vlan_info *p_vinfo);
int br_vlan_get_info_rcu(const struct net_device *dev, u16 vid,
struct bridge_vlan_info *p_vinfo);
+u16 br_vlan_get_offload_info_rcu(const struct net_device *dev,
+ const struct sk_buff *skb, __be16 *proto);
+bool br_vlan_is_enabled_rcu(const struct net_device *dev);
bool br_mst_enabled(const struct net_device *dev);
int br_mst_get_info(const struct net_device *dev, u16 msti, unsigned long *vids);
int br_mst_get_state(const struct net_device *dev, u16 msti, u8 *state);
@@ -184,6 +187,17 @@ static inline int br_vlan_get_info_rcu(const struct net_device *dev, u16 vid,
return -EINVAL;
}
+static inline u16 br_vlan_get_offload_info_rcu(const struct net_device *dev,
+ const struct sk_buff *skb,
+ __be16 *proto)
+{
+ return 0;
+}
+
+static inline bool br_vlan_is_enabled_rcu(const struct net_device *dev)
+{
+ return false;
+}
static inline bool br_mst_enabled(const struct net_device *dev)
{
return false;
@@ -209,6 +223,8 @@ void br_fdb_clear_offload(const struct net_device *dev, u16 vid);
bool br_port_flag_is_set(const struct net_device *dev, unsigned long flag);
u8 br_port_get_stp_state(const struct net_device *dev);
clock_t br_get_ageing_time(const struct net_device *br_dev);
+bool br_fdb_has_forwarding_entry_rcu(const struct net_device *dev,
+ const struct sk_buff *skb, const u8 *addr);
#else
static inline struct net_device *
br_fdb_find_port(const struct net_device *br_dev,
@@ -237,6 +253,13 @@ static inline clock_t br_get_ageing_time(const struct net_device *br_dev)
{
return 0;
}
+
+static inline bool br_fdb_has_forwarding_entry_rcu(const struct net_device *dev,
+ const struct sk_buff *skb,
+ const u8 *addr)
+{
+ return false;
+}
#endif
#endif
diff --git a/net/bridge/br_fdb.c b/net/bridge/br_fdb.c
index e4570bbed854..3161c2689f6a 100644
--- a/net/bridge/br_fdb.c
+++ b/net/bridge/br_fdb.c
@@ -267,6 +267,38 @@ struct net_bridge_fdb_entry *br_fdb_find_rcu(struct net_bridge *br,
return fdb_find_rcu(&br->fdb_hash_tbl, addr, vid);
}
+/**
+ * br_fdb_has_forwarding_entry_rcu - check if a MAC can be forwarded by the bridge
+ * @dev: bridge port network device
+ * @skb: packet buffer (used to determine VLAN id)
+ * @addr: destination MAC address
+ *
+ * Resolves the VLAN id for @skb on @dev (skb VLAN tag when present, PVID
+ * when VLAN filtering is enabled, 0 otherwise) then checks whether the bridge
+ * FDB contains a forwarding entry (dst != NULL, not a local/self entry) for
+ * @addr and that VLAN id. Single br_port_get_rcu() lookup.
+ * Must be called under RCU read lock.
+ */
+bool br_fdb_has_forwarding_entry_rcu(const struct net_device *dev,
+ const struct sk_buff *skb, const u8 *addr)
+{
+ struct net_bridge_port *port = br_port_get_rcu(dev);
+ struct net_bridge_fdb_entry *fdb;
+ u16 vid = 0;
+
+ if (!port)
+ return false;
+ if (br_opt_get(port->br, BROPT_VLAN_ENABLED)) {
+ if (skb_vlan_tag_present(skb))
+ vid = skb_vlan_tag_get_id(skb);
+ else
+ br_vlan_get_pvid_rcu(dev, &vid);
+ }
+ fdb = br_fdb_find_rcu(port->br, addr, vid);
+ return fdb && fdb->dst;
+}
+EXPORT_SYMBOL_GPL(br_fdb_has_forwarding_entry_rcu);
+
/* When a static FDB entry is added, the mac address from the entry is
* added to the bridge private HW address list and all required ports
* are then updated with the new information.
diff --git a/net/bridge/br_vlan.c b/net/bridge/br_vlan.c
index 5560afcaaca3..0b296362adf7 100644
--- a/net/bridge/br_vlan.c
+++ b/net/bridge/br_vlan.c
@@ -1559,6 +1559,51 @@ int br_vlan_get_info_rcu(const struct net_device *dev, u16 vid,
}
EXPORT_SYMBOL_GPL(br_vlan_get_info_rcu);
+/**
+ * br_vlan_get_offload_info_rcu - get VLAN id and protocol for bridge flow offload
+ * @dev: bridge port network device
+ * @skb: packet buffer
+ * @proto: output for the bridge VLAN protocol (set only when return value != 0)
+ *
+ * When VLAN filtering is enabled, resolves the VLAN id for flow offload (skb
+ * VLAN tag id if present, PVID otherwise) and writes the bridge VLAN protocol
+ * to @proto. Returns 0 when filtering is off or @dev is not a bridge port.
+ * Single br_port_get_rcu() lookup. Must be called under RCU read lock.
+ */
+u16 br_vlan_get_offload_info_rcu(const struct net_device *dev,
+ const struct sk_buff *skb, __be16 *proto)
+{
+ struct net_bridge_port *port = br_port_get_rcu(dev);
+ u16 vid = 0;
+
+ if (!port || !br_opt_get(port->br, BROPT_VLAN_ENABLED))
+ return 0;
+ if (skb_vlan_tag_present(skb))
+ vid = skb_vlan_tag_get_id(skb);
+ else
+ br_vlan_get_pvid_rcu(dev, &vid);
+ if (vid)
+ *proto = port->br->vlan_proto;
+ return vid;
+}
+EXPORT_SYMBOL_GPL(br_vlan_get_offload_info_rcu);
+
+/**
+ * br_vlan_is_enabled_rcu - check if VLAN filtering is active on a port's bridge
+ * @dev: bridge port network device
+ *
+ * Returns true if VLAN filtering is enabled on the bridge @dev belongs to.
+ * Returns false when @dev is not a bridge port or filtering is off.
+ * Must be called under RCU read lock.
+ */
+bool br_vlan_is_enabled_rcu(const struct net_device *dev)
+{
+ struct net_bridge_port *port = br_port_get_rcu(dev);
+
+ return port && br_opt_get(port->br, BROPT_VLAN_ENABLED);
+}
+EXPORT_SYMBOL_GPL(br_vlan_is_enabled_rcu);
+
static int br_vlan_is_bind_vlan_dev(const struct net_device *dev)
{
return is_vlan_dev(dev) &&
--
2.54.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 3/5] netfilter: nf_flow_table_path: add L2 bridge offload
2026-06-29 12:32 [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload Daniel Pawlik
2026-06-29 12:32 ` [PATCH 1/5] net: export __dev_fill_forward_path Daniel Pawlik
2026-06-29 12:32 ` [PATCH 2/5] net: bridge: add flow offload helpers Daniel Pawlik
@ 2026-06-29 12:32 ` Daniel Pawlik
2026-06-29 12:32 ` [PATCH 4/5] netfilter: nf_flow_table_path: handle DEV_PATH_MTK_WDMA in path info Daniel Pawlik
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Daniel Pawlik @ 2026-06-29 12:32 UTC (permalink / raw)
To: netfilter-devel, netdev
Cc: pablo, fw, phil, davem, edumazet, kuba, pabeni, horms,
andrew+netdev, razor, idosch, matthias.bgg,
angelogioacchino.delregno, bridge, coreteam, linux-mediatek,
linux-arm-kernel, rchen14b, lorenzo, Daniel Pawlik
From: Ryan Chen <rchen14b@gmail.com>
Allow nft_flow_offload to accelerate traffic forwarded at layer 2 through
Linux bridge ports.
Detection: nft_flow_offload_is_bridging() identifies bridged flows by
checking that the ingress device is a bridge port and that the destination
MAC appears in the bridge FDB with a forwarding destination port (non-local
entry). VLAN resolution and FDB lookup are combined in a single
br_port_get_rcu() call via br_fdb_has_forwarding_entry_rcu().
Routing: nft_flow_route_bridging() allocates minimal dst entries anchored
to the bridge master device via rt_dst_alloc()/ip6_dst_alloc(). A full
routing table lookup via nf_route() is intentionally avoided: it fails for
prefixes that are only bridged, not routed, through the bridge interface
(e.g. when the bridge has no IP address or the bridged subnet is not in
the routing table).
MAC addresses: for bridged flows, nft_dev_forward_path() copies Ethernet
addresses directly from the packet header instead of going through the
neighbour table. Direction (original vs reply) is resolved against the
conntrack direction so both flow directions receive the correct MAC pair.
VLAN context: nft_br_vlan_dev_fill_forward_path() pre-populates the
net_device_path_ctx with the port VLAN id and protocol before the forward
path walk, enabling VLAN-aware hardware offload entries.
Also:
- info->indev is updated for every path type in nft_dev_path_info() so
the bridge ingress device is correctly tracked regardless of path type.
- nft_flow_route() is now a thin dispatcher that delegates to
nft_flow_route_routing() (routed traffic) or nft_flow_route_bridging()
(bridged traffic); the exported API is unchanged.
Path discovery infrastructure was moved to nf_flow_table_path.c in
commit 93d7a7ed0734 ("netfilter: flowtable: move path discovery
infrastructure to its own file"), so all changes land in that file.
Based on a MediaTek SDK patch by Bo-Cun Chen <bc-bocun.chen@mediatek.com>.
Co-developed-by: Daniel Pawlik <pawlik.dan@gmail.com>
Signed-off-by: Daniel Pawlik <pawlik.dan@gmail.com>
Signed-off-by: Ryan Chen <rchen14b@gmail.com>
---
net/netfilter/nf_flow_table_path.c | 167 +++++++++++++++++++++++++++--
1 file changed, 157 insertions(+), 10 deletions(-)
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index 98c03b487f52..6c470854127f 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -15,6 +15,10 @@
#include <net/netfilter/nf_conntrack_core.h>
#include <net/netfilter/nf_conntrack_extend.h>
#include <net/netfilter/nf_flow_table.h>
+#include <linux/if_bridge.h>
+#include <linux/if_ether.h>
+#include <net/route.h>
+#include <net/ip6_route.h>
static enum flow_offload_xmit_type nft_xmit_type(struct dst_entry *dst)
{
@@ -42,7 +46,25 @@ static bool nft_is_valid_ether_device(const struct net_device *dev)
return true;
}
-static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
+static bool nft_flow_offload_is_bridging(struct sk_buff *skb)
+{
+ bool ret;
+
+ if (!netif_is_bridge_port(skb->dev))
+ return false;
+ if (!skb_mac_header_was_set(skb))
+ return false;
+
+ rcu_read_lock();
+ ret = br_fdb_has_forwarding_entry_rcu(skb->dev, skb,
+ eth_hdr(skb)->h_dest);
+ rcu_read_unlock();
+
+ return ret;
+}
+
+static int nft_dev_fill_forward_path(struct net_device_path_ctx *ctx,
+ const struct nf_flow_route *route,
const struct dst_entry *dst_cache,
const struct nf_conn *ct,
enum ip_conntrack_dir dir, u8 *ha,
@@ -58,6 +80,12 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
goto out;
}
+ /* Bridging fastpath copies Ethernet addresses into ha; do not replace
+ * them via neighbour lookup on the routed destination device.
+ */
+ if (!is_zero_ether_addr(ha))
+ goto out;
+
n = dst_neigh_lookup(dst_cache, daddr);
if (!n)
return -1;
@@ -72,7 +100,23 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
return -1;
out:
- return dev_fill_forward_path(dev, ha, stack);
+ return __dev_fill_forward_path(ctx, ha, stack);
+}
+
+static void nft_br_vlan_dev_fill_forward_path(const struct nft_pktinfo *pkt,
+ struct net_device_path_ctx *ctx)
+{
+ __be16 proto = 0;
+ u16 vlan_id;
+
+ rcu_read_lock();
+ vlan_id = br_vlan_get_offload_info_rcu(pkt->skb->dev, pkt->skb, &proto);
+ if (vlan_id) {
+ ctx->num_vlans = 1;
+ ctx->vlan[0].id = vlan_id;
+ ctx->vlan[0].proto = proto;
+ }
+ rcu_read_unlock();
}
struct nft_forward_info {
@@ -103,13 +147,13 @@ static int nft_dev_path_info(const struct net_device_path_stack *stack,
for (i = 0; i < stack->num_paths; i++) {
path = &stack->path[i];
+ info->indev = path->dev;
switch (path->type) {
case DEV_PATH_ETHERNET:
case DEV_PATH_DSA:
case DEV_PATH_VLAN:
case DEV_PATH_PPPOE:
case DEV_PATH_TUN:
- info->indev = path->dev;
if (is_zero_ether_addr(info->h_source))
memcpy(info->h_source, path->dev->dev_addr, ETH_ALEN);
@@ -244,6 +288,7 @@ static int nft_flow_tunnel_update_route(const struct nft_pktinfo *pkt,
}
static int nft_dev_forward_path(const struct nft_pktinfo *pkt,
+ bool is_bridging,
struct nf_flow_route *route,
const struct nf_conn *ct,
enum ip_conntrack_dir dir,
@@ -251,11 +296,33 @@ static int nft_dev_forward_path(const struct nft_pktinfo *pkt,
{
const struct dst_entry *dst = route->tuple[dir].dst;
struct net_device_path_stack stack;
+ struct net_device_path_ctx ctx = {
+ .dev = dst->dev,
+ };
struct nft_forward_info info = {};
+ enum ip_conntrack_info pkt_ctinfo;
+ enum ip_conntrack_dir skb_dir;
+ struct ethhdr *eth;
unsigned char ha[ETH_ALEN];
int i;
- if (nft_dev_fill_forward_path(route, dst, ct, dir, ha, &stack) < 0 ||
+ memset(ha, 0, sizeof(ha));
+
+ if (is_bridging) {
+ nf_ct_get(pkt->skb, &pkt_ctinfo);
+ eth = eth_hdr(pkt->skb);
+ skb_dir = CTINFO2DIR(pkt_ctinfo);
+ if (skb_dir != dir) {
+ memcpy(ha, eth->h_source, ETH_ALEN);
+ memcpy(info.h_source, eth->h_dest, ETH_ALEN);
+ } else {
+ memcpy(ha, eth->h_dest, ETH_ALEN);
+ memcpy(info.h_source, eth->h_source, ETH_ALEN);
+ }
+ nft_br_vlan_dev_fill_forward_path(pkt, &ctx);
+ }
+
+ if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) < 0 ||
nft_dev_path_info(&stack, &info, ha, &ft->data) < 0)
return -ENOENT;
@@ -292,9 +359,11 @@ static int nft_dev_forward_path(const struct nft_pktinfo *pkt,
return 0;
}
-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)
+static int nft_flow_route_routing(const struct nft_pktinfo *pkt,
+ const struct nf_conn *ct,
+ struct nf_flow_route *route,
+ enum ip_conntrack_dir dir,
+ struct nft_flowtable *ft)
{
struct dst_entry *this_dst = skb_dst(pkt->skb);
struct dst_entry *other_dst = NULL;
@@ -334,12 +403,12 @@ int nft_flow_route(const struct nft_pktinfo *pkt, const struct nf_conn *ct,
nft_default_forward_path(route, this_dst, dir);
nft_default_forward_path(route, other_dst, !dir);
- if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH &&
- nft_dev_forward_path(pkt, route, ct, dir, ft) < 0)
+ if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH &&
+ nft_dev_forward_path(pkt, false, route, ct, dir, ft) < 0)
goto err_dst_release;
if (route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH &&
- nft_dev_forward_path(pkt, route, ct, !dir, ft) < 0)
+ nft_dev_forward_path(pkt, false, route, ct, !dir, ft) < 0)
goto err_dst_release;
return 0;
@@ -349,4 +418,82 @@ int nft_flow_route(const struct nft_pktinfo *pkt, const struct nf_conn *ct,
dst_release(route->tuple[!dir].dst);
return -ENOENT;
}
+
+static int nft_flow_route_bridging(const struct nft_pktinfo *pkt,
+ const struct nf_conn *ct,
+ struct nf_flow_route *route,
+ enum ip_conntrack_dir dir,
+ struct nft_flowtable *ft)
+{
+ struct dst_entry *dsts[IP_CT_DIR_MAX] = {};
+ struct net_device *br_dev;
+ int i;
+
+ /* Allocate minimal dsts anchored to the bridge master device to supply
+ * xmit_type and MTU. A full routing lookup via nf_route() is avoided
+ * because it fails for prefixes that are bridged but not routed.
+ */
+ rcu_read_lock();
+ br_dev = netdev_master_upper_dev_get_rcu(pkt->skb->dev);
+ if (!br_dev || !netif_is_bridge_master(br_dev)) {
+ rcu_read_unlock();
+ return -ENOENT;
+ }
+
+ for (i = 0; i < IP_CT_DIR_MAX; i++) {
+ switch (nft_pf(pkt)) {
+ case NFPROTO_IPV4: {
+ struct rtable *rt;
+
+ rt = rt_dst_alloc(br_dev, 0, RTN_UNICAST, true);
+ if (rt)
+ dsts[i] = &rt->dst;
+ break;
+ }
+ case NFPROTO_IPV6: {
+ struct rt6_info *rt;
+
+ rt = ip6_dst_alloc(nft_net(pkt), br_dev, 0);
+ if (rt)
+ dsts[i] = &rt->dst;
+ break;
+ }
+ }
+ }
+ rcu_read_unlock();
+
+ if (!dsts[dir] || !dsts[!dir]) {
+ dst_release(dsts[dir]);
+ dst_release(dsts[!dir]);
+ return -ENOENT;
+ }
+
+ nft_default_forward_path(route, dsts[dir], dir);
+ nft_default_forward_path(route, dsts[!dir], !dir);
+ /* Drop allocation references; route->tuple[*].dst holds the clones. */
+ dst_release(dsts[dir]);
+ dst_release(dsts[!dir]);
+
+ if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH &&
+ route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH) {
+ if (nft_dev_forward_path(pkt, true, route, ct, dir, ft) ||
+ nft_dev_forward_path(pkt, true, route, ct, !dir, ft)) {
+ dst_release(route->tuple[dir].dst);
+ dst_release(route->tuple[!dir].dst);
+ return -ENOENT;
+ }
+ }
+
+ return 0;
+}
+
+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)
+{
+ if (nft_flow_offload_is_bridging(pkt->skb))
+ return nft_flow_route_bridging(pkt, ct, route, dir, ft);
+
+ return nft_flow_route_routing(pkt, ct, route, dir, ft);
+}
EXPORT_SYMBOL_GPL(nft_flow_route);
--
2.54.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 4/5] netfilter: nf_flow_table_path: handle DEV_PATH_MTK_WDMA in path info
2026-06-29 12:32 [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload Daniel Pawlik
` (2 preceding siblings ...)
2026-06-29 12:32 ` [PATCH 3/5] netfilter: nf_flow_table_path: add L2 bridge offload Daniel Pawlik
@ 2026-06-29 12:32 ` Daniel Pawlik
2026-06-29 12:32 ` [PATCH 5/5] netfilter: nf_flow_table_path: add VLAN passthrough support Daniel Pawlik
2026-06-29 12:56 ` [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload Florian Westphal
5 siblings, 0 replies; 7+ messages in thread
From: Daniel Pawlik @ 2026-06-29 12:32 UTC (permalink / raw)
To: netfilter-devel, netdev
Cc: pablo, fw, phil, davem, edumazet, kuba, pabeni, horms,
andrew+netdev, razor, idosch, matthias.bgg,
angelogioacchino.delregno, bridge, coreteam, linux-mediatek,
linux-arm-kernel, rchen14b, lorenzo, Daniel Pawlik
From: Ryan Chen <rchen14b@gmail.com>
Without this change, nft_dev_path_info() hits the default -ENOENT path
for WiFi bridge offload via WDMA on MT7996. When a bridged flow goes
through the MT7996 WiFi device, the DEV_PATH_MTK_WDMA step does not set
h_source, causing the PPE entry to receive a zero source MAC and packets
to stall in both software fastpath and hardware path.
Based on a MediaTek SDK patch by Bo-Cun Chen <bc-bocun.chen@mediatek.com>.
Signed-off-by: Ryan Chen <rchen14b@gmail.com>
Signed-off-by: Daniel Pawlik <pawlik.dan@gmail.com>
---
net/netfilter/nf_flow_table_path.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index 6c470854127f..580aa1db3cb4 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -219,6 +219,10 @@ static int nft_dev_path_info(const struct net_device_path_stack *stack,
}
info->xmit_type = FLOW_OFFLOAD_XMIT_DIRECT;
break;
+ case DEV_PATH_MTK_WDMA:
+ if (is_zero_ether_addr(info->h_source))
+ memcpy(info->h_source, path->dev->dev_addr, ETH_ALEN);
+ break;
default:
return -1;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 5/5] netfilter: nf_flow_table_path: add VLAN passthrough support
2026-06-29 12:32 [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload Daniel Pawlik
` (3 preceding siblings ...)
2026-06-29 12:32 ` [PATCH 4/5] netfilter: nf_flow_table_path: handle DEV_PATH_MTK_WDMA in path info Daniel Pawlik
@ 2026-06-29 12:32 ` Daniel Pawlik
2026-06-29 12:56 ` [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload Florian Westphal
5 siblings, 0 replies; 7+ messages in thread
From: Daniel Pawlik @ 2026-06-29 12:32 UTC (permalink / raw)
To: netfilter-devel, netdev
Cc: pablo, fw, phil, davem, edumazet, kuba, pabeni, horms,
andrew+netdev, razor, idosch, matthias.bgg,
angelogioacchino.delregno, bridge, coreteam, linux-mediatek,
linux-arm-kernel, rchen14b, lorenzo, Daniel Pawlik
From: Ryan Chen <rchen14b@gmail.com>
VLAN passthrough packets can be offloaded when bridge-nf-filter-vlan-tagged
is enabled. When a packet has a VLAN tag and the bridge does not have VLAN
filtering enabled (passthrough mode), record the VLAN encap info so the
hardware flow offload entry includes the correct VLAN tag.
Without this change, VLAN-tagged bridged traffic cannot be offloaded by PPE
because the VLAN encap information is missing from the flow entry.
Enable with: echo 1 > /proc/sys/net/bridge/bridge-nf-filter-vlan-tagged
Based on a MediaTek SDK patch by Chak-Kei Lam <chak-kei.lam@mediatek.com>.
Signed-off-by: Ryan Chen <rchen14b@gmail.com>
Signed-off-by: Daniel Pawlik <pawlik.dan@gmail.com>
---
net/netfilter/nf_flow_table_path.c | 32 ++++++++++++++++++++++++++++--
1 file changed, 30 insertions(+), 2 deletions(-)
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index 580aa1db3cb4..d15c425c88c4 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -17,6 +17,7 @@
#include <net/netfilter/nf_flow_table.h>
#include <linux/if_bridge.h>
#include <linux/if_ether.h>
+#include <linux/if_vlan.h>
#include <net/route.h>
#include <net/ip6_route.h>
@@ -136,6 +137,29 @@ struct nft_forward_info {
enum flow_offload_xmit_type xmit_type;
};
+static void nft_fill_vlan_passthrough_info(const struct nft_pktinfo *pkt,
+ struct nft_forward_info *info)
+{
+ if (!skb_vlan_tag_present(pkt->skb))
+ return;
+
+ rcu_read_lock();
+ /* when bridge VLAN filtering is enabled, the bridge handles the tag */
+ if (netif_is_bridge_port(pkt->skb->dev) &&
+ !br_vlan_is_enabled_rcu(pkt->skb->dev)) {
+ if (info->num_encaps >= NF_FLOW_TABLE_ENCAP_MAX) {
+ info->indev = NULL;
+ } else {
+ info->encap[info->num_encaps].id =
+ skb_vlan_tag_get_id(pkt->skb);
+ info->encap[info->num_encaps].proto =
+ pkt->skb->vlan_proto;
+ info->num_encaps++;
+ }
+ }
+ rcu_read_unlock();
+}
+
static int nft_dev_path_info(const struct net_device_path_stack *stack,
struct nft_forward_info *info,
unsigned char *ha, struct nf_flowtable *flowtable)
@@ -326,8 +350,12 @@ static int nft_dev_forward_path(const struct nft_pktinfo *pkt,
nft_br_vlan_dev_fill_forward_path(pkt, &ctx);
}
- if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) < 0 ||
- nft_dev_path_info(&stack, &info, ha, &ft->data) < 0)
+ if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) < 0)
+ return -ENOENT;
+
+ nft_fill_vlan_passthrough_info(pkt, &info);
+
+ if (nft_dev_path_info(&stack, &info, ha, &ft->data) < 0)
return -ENOENT;
if (!nft_flowtable_find_dev(info.indev, ft))
--
2.54.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload
2026-06-29 12:32 [PATCH 0/5] netfilter: nf_flow_table_path: L2 bridge offload Daniel Pawlik
` (4 preceding siblings ...)
2026-06-29 12:32 ` [PATCH 5/5] netfilter: nf_flow_table_path: add VLAN passthrough support Daniel Pawlik
@ 2026-06-29 12:56 ` Florian Westphal
5 siblings, 0 replies; 7+ messages in thread
From: Florian Westphal @ 2026-06-29 12:56 UTC (permalink / raw)
To: Daniel Pawlik
Cc: netfilter-devel, netdev, pablo, phil, davem, edumazet, kuba,
pabeni, horms, andrew+netdev, razor, idosch, matthias.bgg,
angelogioacchino.delregno, bridge, coreteam, linux-mediatek,
linux-arm-kernel, rchen14b, lorenzo
Daniel Pawlik <pawlik.dan@gmail.com> wrote:
> -----------------------------
> 1. Load kmod-br-netfilter so that bridged IP traffic traverses the
> netfilter forward chain.
Ouch. br_netfilter should die. Really. Its a gross hack, never
use this thing...
> 2. Enable netfilter hooks on the bridge:
> echo 1 > /sys/class/net/<br>/bridge/nf_call_iptables
> echo 1 > /sys/class/net/<br>/bridge/nf_call_ip6tables
>
> 3. Register bridge member interfaces in the nft flowtable:
> table inet filter {
> flowtable f {
> hook ingress priority filter
> devices = { eth0, wlan0 }
> }
I think that bridge flowtable should use 'table bridge ...', not
use the br_netfilter compat hacks.
Sorry.
Are you aware of Eric Woudstras bridge flowtable patches?
https://lore.kernel.org/netfilter-devel/20250408142802.96101-5-ericwouds@gmail.com/
^ permalink raw reply [flat|nested] 7+ messages in thread