* [PATCH nf-next,v2 0/3] initial flowtable bridge support
@ 2026-07-10 10:07 Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
` (2 more replies)
0 siblings, 3 replies; 12+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-10 10:07 UTC (permalink / raw)
To: netfilter-devel; +Cc: razor, ericwouds, fw
Hi,
This series adds initial support for the flowtable bridge family.
The goal is to allow to set up a forwarding path between bridge ports
which is not possible with the existing infrastructure.
This series does not support for VLAN/PPPoe tagged packets circulating
in the bridge, ie. packets are seen untagged from the ingress path of
the bridge ports.
Note that this does not include bridge vlan filtering which needs
a nf_conntrack_bridge enhancement to support PPPoE/VLAN natively, so
I can keeping back those patches
Patch #1 and #2 are preparation patches, not strictly necessary at
this stage but they will be needed once the flowtable bridge can handle
tagged PPPoE/VLAN.
Patch #3 adds the initial flowtable bridge supports. This intentional
adds a new dataplane for the flowtable bridge, which is more boilerplate
code, rather than tweaking the existing flowtable IP dataplane. This is
intentional, for maintainability and extensibility reasons. Similarly
a new flow_offload expression is added for the bridge family.
Comments welcome, thanks.
Pablo Neira Ayuso (3):
net: pass net_device_path_ctx struct to dev_fill_forward_path()
net: expose dev_fwd_path() helper via static inline
netfilter: flowtable: initial bridge support
include/linux/netdevice.h | 13 ++-
include/net/netfilter/nf_flow_table.h | 7 ++
net/core/dev.c | 28 ++----
net/netfilter/nf_flow_table_inet.c | 12 +++
net/netfilter/nf_flow_table_ip.c | 134 ++++++++++++++++++++++++++
net/netfilter/nf_flow_table_path.c | 79 ++++++++++++++-
net/netfilter/nft_flow_offload.c | 88 ++++++++++++++++-
7 files changed, 336 insertions(+), 25 deletions(-)
--
2.47.3
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path()
2026-07-10 10:07 [PATCH nf-next,v2 0/3] initial flowtable bridge support Pablo Neira Ayuso
@ 2026-07-10 10:07 ` Pablo Neira Ayuso
2026-07-11 9:30 ` Eric Woudstra
2026-07-12 9:28 ` Eric Woudstra
2026-07-10 10:07 ` [PATCH nf-next,v2 2/3] net: expose dev_fwd_path() helper via static inline Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support Pablo Neira Ayuso
2 siblings, 2 replies; 12+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-10 10:07 UTC (permalink / raw)
To: netfilter-devel; +Cc: razor, ericwouds, fw
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>
---
v2: - move nft_dev_fill_forward_path_init() call after out: goto tag
to fix a crash otherwise in the existing flowtable ip family.
include/linux/netdevice.h | 2 +-
net/core/dev.c | 18 +++++++-----------
net/netfilter/nf_flow_table_path.c | 14 ++++++++++++--
3 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 9981d637f8b5..db04b6d2e8d2 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 714d05283500..24c384ef9e78 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..5455149e5d9a 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;
@@ -70,9 +79,10 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
if (!(nud_state & NUD_VALID))
return -1;
-
out:
- return dev_fill_forward_path(dev, ha, stack);
+ nft_dev_fill_forward_path_init(&ctx, dev, ha);
+
+ return dev_fill_forward_path(&ctx, stack);
}
struct nft_forward_info {
--
2.47.3
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH nf-next,v2 2/3] net: expose dev_fwd_path() helper via static inline
2026-07-10 10:07 [PATCH nf-next,v2 0/3] initial flowtable bridge support Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
@ 2026-07-10 10:07 ` Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support Pablo Neira Ayuso
2 siblings, 0 replies; 12+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-10 10:07 UTC (permalink / raw)
To: netfilter-devel; +Cc: razor, ericwouds, fw
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>
---
v2: no changes
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 db04b6d2e8d2..3793a8c0b0be 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 24c384ef9e78..48288a5dc870 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] 12+ messages in thread
* [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support
2026-07-10 10:07 [PATCH nf-next,v2 0/3] initial flowtable bridge support Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 2/3] net: expose dev_fwd_path() helper via static inline Pablo Neira Ayuso
@ 2026-07-10 10:07 ` Pablo Neira Ayuso
2026-07-12 9:27 ` Eric Woudstra
2 siblings, 1 reply; 12+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-10 10:07 UTC (permalink / raw)
To: netfilter-devel; +Cc: razor, ericwouds, fw
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, as long as packets that are
untagged when circulating within the bridge.
This initial bridge flowtable support does support VLAN tagged packets
circulating within the bridge yet, because nf_conntrack_bridge still
does not support PPPoE/VLAN natively.
Hardware offload is disabled until there is a driver in the tree
supporting this.
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
v2: remove bridge vlan support, currently not exercised.
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_path.c | 65 +++++++++++++
net/netfilter/nft_flow_offload.c | 88 ++++++++++++++++-
5 files changed, 305 insertions(+), 1 deletion(-)
diff --git a/include/net/netfilter/nf_flow_table.h b/include/net/netfilter/nf_flow_table.h
index 7b23b245a5a8..d65914198ec9 100644
--- a/include/net/netfilter/nf_flow_table.h
+++ b/include/net/netfilter/nf_flow_table.h
@@ -247,6 +247,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,
@@ -341,6 +343,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))
@@ -374,6 +378,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 29e93ac1e2e4..17ae49f62aa5 100644
--- a/net/netfilter/nf_flow_table_ip.c
+++ b/net/netfilter/nf_flow_table_ip.c
@@ -1196,3 +1196,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_path.c b/net/netfilter/nf_flow_table_path.c
index 5455149e5d9a..a3aa9a9ce673 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,67 @@ 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 *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_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;
+ 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] 12+ messages in thread
* Re: [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path()
2026-07-10 10:07 ` [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
@ 2026-07-11 9:30 ` Eric Woudstra
2026-07-12 9:28 ` Eric Woudstra
1 sibling, 0 replies; 12+ messages in thread
From: Eric Woudstra @ 2026-07-11 9:30 UTC (permalink / raw)
To: Pablo Neira Ayuso, netfilter-devel; +Cc: razor, fw
On 7/10/26 12:07 PM, Pablo Neira Ayuso wrote:
> 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>
> ---
> v2: - move nft_dev_fill_forward_path_init() call after out: goto tag
> to fix a crash otherwise in the existing flowtable ip family.
>
> include/linux/netdevice.h | 2 +-
> net/core/dev.c | 18 +++++++-----------
> net/netfilter/nf_flow_table_path.c | 14 ++++++++++++--
> 3 files changed, 20 insertions(+), 14 deletions(-)
>
> diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
> index 9981d637f8b5..db04b6d2e8d2 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);
dev_fill_forward_path is also used in mtk_ppe_offload.c and airoha_ppe.c
This is the build error for mtk_ppe_offload.c:
drivers/net/ethernet/mediatek/mtk_ppe_offload.c: In function
'mtk_flow_get_wdma_info':
drivers/net/ethernet/mediatek/mtk_ppe_offload.c:105:37: error: passing
argument 1 of 'dev_fill_forward_path' from incompatible pointer type
[-Wincompatible-pointer-types]
105 | err = dev_fill_forward_path(dev, addr, &stack);
| ^~~
| |
| struct net_device *
In file included from ./include/net/sock.h:46,
from ./include/linux/tcp.h:19,
from ./include/linux/ipv6.h:103,
from drivers/net/ethernet/mediatek/mtk_ppe_offload.c:9:
./include/linux/netdevice.h:3435:55: note: expected 'struct
net_device_path_ctx *' but argument is of type 'struct net_device *'
3435 | int dev_fill_forward_path(struct net_device_path_ctx *ctx,
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~
drivers/net/ethernet/mediatek/mtk_ppe_offload.c:105:42: error: passing
argument 2 of 'dev_fill_forward_path' from incompatible pointer type
[-Wincompatible-pointer-types]
105 | err = dev_fill_forward_path(dev, addr, &stack);
| ^~~~
| |
| const u8 * {aka const
unsigned char *}
./include/linux/netdevice.h:3436:57: note: expected 'struct
net_device_path_stack *' but argument is of type 'const u8 *' {aka
'const unsigned char *'}
3436 | struct net_device_path_stack *stack);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
drivers/net/ethernet/mediatek/mtk_ppe_offload.c:105:15: error: too many
arguments to function 'dev_fill_forward_path'; expected 2, have 3
105 | err = dev_fill_forward_path(dev, addr, &stack);
| ^~~~~~~~~~~~~~~~~~~~~ ~~~~~~
./include/linux/netdevice.h:3435:5: note: declared here
3435 | int dev_fill_forward_path(struct net_device_path_ctx *ctx,
| ^~~~~~~~~~~~~~~~~~~~~
make[6]: *** [scripts/Makefile.build:289:
drivers/net/ethernet/mediatek/mtk_ppe_offload.o] Error 1
make[5]: *** [scripts/Makefile.build:549: drivers/net/ethernet/mediatek]
Error 2
make[4]: *** [scripts/Makefile.build:549: drivers/net/ethernet] Error 2
> diff --git a/net/core/dev.c b/net/core/dev.c
> index 714d05283500..24c384ef9e78 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..5455149e5d9a 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;
>
> @@ -70,9 +79,10 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
>
> if (!(nud_state & NUD_VALID))
> return -1;
> -
> out:
> - return dev_fill_forward_path(dev, ha, stack);
> + nft_dev_fill_forward_path_init(&ctx, dev, ha);
> +
> + return dev_fill_forward_path(&ctx, stack);
> }
>
> struct nft_forward_info {
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support
2026-07-10 10:07 ` [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support Pablo Neira Ayuso
@ 2026-07-12 9:27 ` Eric Woudstra
2026-07-12 13:46 ` Eric Woudstra
` (2 more replies)
0 siblings, 3 replies; 12+ messages in thread
From: Eric Woudstra @ 2026-07-12 9:27 UTC (permalink / raw)
To: Pablo Neira Ayuso, netfilter-devel; +Cc: razor, fw
On 7/10/26 12:07 PM, Pablo Neira Ayuso wrote:
> 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, as long as packets that are
> untagged when circulating within the bridge.
>
> This initial bridge flowtable support does support VLAN tagged packets
> circulating within the bridge yet, because nf_conntrack_bridge still
> does not support PPPoE/VLAN natively.
>
> Hardware offload is disabled until there is a driver in the tree
> supporting this.
>
> Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
> ---
> v2: remove bridge vlan support, currently not exercised.
>
> 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_path.c | 65 +++++++++++++
> net/netfilter/nft_flow_offload.c | 88 ++++++++++++++++-
> 5 files changed, 305 insertions(+), 1 deletion(-)
>
> diff --git a/include/net/netfilter/nf_flow_table.h b/include/net/netfilter/nf_flow_table.h
> index 7b23b245a5a8..d65914198ec9 100644
> --- a/include/net/netfilter/nf_flow_table.h
> +++ b/include/net/netfilter/nf_flow_table.h
> @@ -247,6 +247,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,
> @@ -341,6 +343,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))
> @@ -374,6 +378,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 29e93ac1e2e4..17ae49f62aa5 100644
> --- a/net/netfilter/nf_flow_table_ip.c
> +++ b/net/netfilter/nf_flow_table_ip.c
> @@ -1196,3 +1196,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_path.c b/net/netfilter/nf_flow_table_path.c
> index 5455149e5d9a..a3aa9a9ce673 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,67 @@ 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 *dev,
> + unsigned char *src_ha,
> + unsigned char *dst_ha)
> +{
> + struct flow_offload_tuple *this_tuple = &flow->tuplehash[dir].tuple;
Add:
struct flow_offload_tuple *other_tuple = &flow->tuplehash[!dir].tuple;
See below.
> + 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);
> +
Here you could add the following to handle the encaps on this_tuple.
for (i = this_tuple->encap_num - 1; i >= 0 ; i--) {
if (info.num_encaps >= NF_FLOW_TABLE_ENCAP_MAX)
return -1;
if (this_tuple->in_vlan_ingress & BIT(i))
continue;
info.encap[info.num_encaps].id = this_tuple->encap[i].id;
info.encap[info.num_encaps].proto = this_tuple->encap[i].proto;
info.num_encaps++;
if (this_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 = this_tuple->encap[i].id;
ctx.vlan[ctx.num_vlans].proto = this_tuple->encap[i].proto;
ctx.num_vlans++;
}
> + if (dev_fill_forward_path(&ctx, &stack) < 0 ||
> + nft_dev_path_info(&stack, &info, dst_ha, &ft->data) < 0)
> + return -1;
> +
> + if (!nft_flowtable_find_dev(info.indev, ft))
> + return -1;
> +
After replacing dev_fill_forward_path() with dev_fill_bridge_path(),
from here...
> + 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;
> + j++;
> + }
> + this_tuple->encap_num = info.num_encaps;
Until here, this_tuple needs to be the other_tuple.
dev_fill_forward_path() does not traverse the bridge.
See other comment in other patch. Also, need to copy
the in_vlan_ingress bit.
So it becomes:
other_tuple->iifidx = info.indev->ifindex;
for (i = info.num_encaps - 1; i >= 0; i--) {
other_tuple->encap[j].id = info.encap[i].id;
other_tuple->encap[j].proto = info.encap[i].proto;
if (info.ingress_vlans & BIT(i))
other_tuple->in_vlan_ingress |= BIT(j);
j++;
}
other_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;
> +
Here I use the skb to fill other_tuple->encaps. I understand you want to
do this differently.
Then I call nft_dev_fill_bridge_path() with !dir first, then dir.
> + 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;
This could move to nft_dev_fill_bridge_path() (only 1 line) as both
tuples are also known there.
> +
> + 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:
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path()
2026-07-10 10:07 ` [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
2026-07-11 9:30 ` Eric Woudstra
@ 2026-07-12 9:28 ` Eric Woudstra
2026-07-12 19:22 ` Pablo Neira Ayuso
1 sibling, 1 reply; 12+ messages in thread
From: Eric Woudstra @ 2026-07-12 9:28 UTC (permalink / raw)
To: Pablo Neira Ayuso, netfilter-devel; +Cc: razor, fw
On 7/10/26 12:07 PM, Pablo Neira Ayuso wrote:
> 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>
> ---
> v2: - move nft_dev_fill_forward_path_init() call after out: goto tag
> to fix a crash otherwise in the existing flowtable ip family.
>
> include/linux/netdevice.h | 2 +-
> net/core/dev.c | 18 +++++++-----------
> net/netfilter/nf_flow_table_path.c | 14 ++++++++++++--
> 3 files changed, 20 insertions(+), 14 deletions(-)
>
> diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
> index 9981d637f8b5..db04b6d2e8d2 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 714d05283500..24c384ef9e78 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;
> }
Calling dev_fill_forward_path() from nft_dev_fill_bridge_path()
it does not traverse the bridge. It exits at the source "indev"
already. After calling nft_dev_path_info(), the info structure
is filled in with the opposite device and the encaps of the
opposite device.
Would need to add something like this:
int dev_fill_bridge_path(struct net_device_path_ctx *ctx,
struct net_device_path_stack *stack)
{
const struct net_device *last_dev, *br_dev;
struct net_device_path *path;
if (!ctx->dev || !netif_is_bridge_port(ctx->dev))
return -1;
br_dev = netdev_master_upper_dev_get_rcu((struct net_device *)ctx->dev);
if (!br_dev || !br_dev->netdev_ops->ndo_fill_forward_path)
return -1;
last_dev = ctx->dev;
path = dev_fwd_path(stack);
if (!path)
return -1;
memset(path, 0, sizeof(struct net_device_path));
if (br_dev->netdev_ops->ndo_fill_forward_path(ctx, path) < 0)
return -1;
if (!ctx->dev || WARN_ON_ONCE(last_dev == ctx->dev))
return -1;
return dev_fill_forward_path(ctx, stack);
}
EXPORT_SYMBOL_GPL(dev_fill_bridge_path);
First need to find the bridge device to call ndo_fill_forward_path()
from it.
This also needs the patch "bridge: Add filling forward path from port to
port"
That patch still needs a change according to Nikolay.
> diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
> index 98c03b487f52..5455149e5d9a 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));
Need to move:
stack->num_paths = 0;
To here, remove it from dev_fill_forward_path().
> +}
> +
> 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;
>
> @@ -70,9 +79,10 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route,
>
> if (!(nud_state & NUD_VALID))
> return -1;
> -
> out:
> - return dev_fill_forward_path(dev, ha, stack);
> + nft_dev_fill_forward_path_init(&ctx, dev, ha);
> +
> + return dev_fill_forward_path(&ctx, stack);
> }
>
> struct nft_forward_info {
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support
2026-07-12 9:27 ` Eric Woudstra
@ 2026-07-12 13:46 ` Eric Woudstra
2026-07-12 19:13 ` Pablo Neira Ayuso
2026-07-12 19:19 ` Pablo Neira Ayuso
2026-07-13 8:24 ` Pablo Neira Ayuso
2 siblings, 1 reply; 12+ messages in thread
From: Eric Woudstra @ 2026-07-12 13:46 UTC (permalink / raw)
To: Pablo Neira Ayuso, netfilter-devel; +Cc: razor, fw
On 7/12/26 11:27 AM, Eric Woudstra wrote:
>
>
> On 7/10/26 12:07 PM, Pablo Neira Ayuso wrote:
>> 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, as long as packets that are
>> untagged when circulating within the bridge.
>>
>> This initial bridge flowtable support does support VLAN tagged packets
>> circulating within the bridge yet, because nf_conntrack_bridge still
>> does not support PPPoE/VLAN natively.
>>
>> Hardware offload is disabled until there is a driver in the tree
>> supporting this.
>>
>> Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
>> ---
>> v2: remove bridge vlan support, currently not exercised.
>>
>> 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_path.c | 65 +++++++++++++
>> net/netfilter/nft_flow_offload.c | 88 ++++++++++++++++-
>> 5 files changed, 305 insertions(+), 1 deletion(-)
>>
>> diff --git a/include/net/netfilter/nf_flow_table.h b/include/net/netfilter/nf_flow_table.h
>> index 7b23b245a5a8..d65914198ec9 100644
>> --- a/include/net/netfilter/nf_flow_table.h
>> +++ b/include/net/netfilter/nf_flow_table.h
>> @@ -247,6 +247,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,
>> @@ -341,6 +343,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))
>> @@ -374,6 +378,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,
After fixing the problems in the patch-set mentioned earlier, when the sotware
fastpath is setup correctly:
Setting up a hardware offloaded flow, because the hardware supports it,
this crashes:
[ 283.380108] Unable to handle kernel execute from non-executable memory at virtual address 0000000000000000
[ 283.389925] Mem abort info:
[ 283.393178] ESR = 0x0000000086000004
[ 283.396940] EC = 0x21: IABT (current EL), IL = 32 bits
[ 283.402299] SET = 0, FnV = 0
[ 283.405358] EA = 0, S1PTW = 0
[ 283.408498] FSC = 0x04: level 0 translation fault
[ 283.413473] user pgtable: 4k pages, 48-bit VAs, pgdp=00000000419d9000
[ 283.419918] [0000000000000000] pgd=0000000000000000, p4d=0000000000000000
[ 283.426766] Internal error: Oops: 0000000086000004 [#1] SMP
[ 283.432432] Modules linked in: nft_flow_offload nf_flow_table_inet nf_flow_table nft_masq nft_chain_nat nf_nat nf_conntrack_bridge nft_ct nf_conntrack nf_defrag_ipv6 nf_defrag_ipv4 nf_tables cdc_mbim cdc_wdm cdc_ncm r8153_ecm cdc_ether usbnet r8152 mii mt7915e mt76_connac_lib mt76 mac80211 cfg80211 rfkill libarc4
[ 283.460181] CPU: 1 UID: 0 PID: 2420 Comm: kworker/u16:6 Not tainted 7.2.0-rc1-bpirnn #12 PREEMPT
[ 283.469040] Hardware name: Bananapi BPI-R3 (DT)
[ 283.473558] Workqueue: nf_ft_offload_add flow_offload_work_handler [nf_flow_table]
[ 283.481128] pstate: 60400005 (nZCv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--)
[ 283.488084] pc : 0x0
[ 283.490269] lr : nf_flow_offload_rule_alloc+0x45c/0x4b0 [nf_flow_table]
[ 283.496875] sp : ffff8000830b3c90
[ 283.500176] x29: ffff8000830b3c90 x28: ffff00000afc06b8 x27: ffff000007a8a180
[ 283.507298] x26: ffff000006704000 x25: ffff000006704000 x24: ffff0000029598c8
[ 283.514420] x23: ffff00000af7ac50 x22: 0000000000000000 x21: ffff000006704000
[ 283.521541] x20: ffff000002959850 x19: ffff000002959800 x18: 0000000000000000
[ 283.528662] x17: 0000000000000000 x16: 0000000000000000 x15: 0000000000000000
[ 283.535785] x14: 0000000000000000 x13: 0000000000000008 x12: 0101010101010101
[ 283.542914] x11: 7f7f7f7f7f7f7f7f x10: fefefefefefeff63 x9 : 0000000000000000
[ 283.550038] x8 : ffff000001f25000 x7 : 0000000000000000 x6 : 000000000000003f
[ 283.557159] x5 : 00000000ffffffff x4 : 0000000000000000 x3 : ffff000002959800
[ 283.564280] x2 : 0000000000000000 x1 : ffff000006704000 x0 : ffff000007a8a180
[ 283.571405] Call trace:
[ 283.573841] 0x0 (P)
[ 283.576022] flow_offload_work_handler+0x60/0x358 [nf_flow_table]
[ 283.582111] process_scheduled_works+0x210/0x30c
[ 283.586731] worker_thread+0x140/0x1d4
[ 283.590478] kthread+0xf8/0x108
[ 283.593617] ret_from_fork+0x10/0x20
Adding .action = nf_flow_rule_bridge, with the function as it is in my latest
patch named "netfilter: nf_flow_table_offload: Add nf_flow_rule_bridge()",
it does not crash and the hardware offloaded path functions like a charm.
>> + .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 29e93ac1e2e4..17ae49f62aa5 100644
>> --- a/net/netfilter/nf_flow_table_ip.c
>> +++ b/net/netfilter/nf_flow_table_ip.c
>> @@ -1196,3 +1196,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_path.c b/net/netfilter/nf_flow_table_path.c
>> index 5455149e5d9a..a3aa9a9ce673 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,67 @@ 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 *dev,
>> + unsigned char *src_ha,
>> + unsigned char *dst_ha)
>> +{
>> + struct flow_offload_tuple *this_tuple = &flow->tuplehash[dir].tuple;
>
> Add:
>
> struct flow_offload_tuple *other_tuple = &flow->tuplehash[!dir].tuple;
>
> See below.
>
>> + 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);
>> +
>
> Here you could add the following to handle the encaps on this_tuple.
>
> for (i = this_tuple->encap_num - 1; i >= 0 ; i--) {
> if (info.num_encaps >= NF_FLOW_TABLE_ENCAP_MAX)
> return -1;
>
> if (this_tuple->in_vlan_ingress & BIT(i))
> continue;
>
> info.encap[info.num_encaps].id = this_tuple->encap[i].id;
> info.encap[info.num_encaps].proto = this_tuple->encap[i].proto;
> info.num_encaps++;
>
> if (this_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 = this_tuple->encap[i].id;
> ctx.vlan[ctx.num_vlans].proto = this_tuple->encap[i].proto;
> ctx.num_vlans++;
> }
>
>> + if (dev_fill_forward_path(&ctx, &stack) < 0 ||
>> + nft_dev_path_info(&stack, &info, dst_ha, &ft->data) < 0)
>> + return -1;
>> +
>> + if (!nft_flowtable_find_dev(info.indev, ft))
>> + return -1;
>> +
>
> After replacing dev_fill_forward_path() with dev_fill_bridge_path(),
> from here...
>
>> + 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;
>> + j++;
>> + }
>> + this_tuple->encap_num = info.num_encaps;
>
> Until here, this_tuple needs to be the other_tuple.
> dev_fill_forward_path() does not traverse the bridge.
> See other comment in other patch. Also, need to copy
> the in_vlan_ingress bit.
>
> So it becomes:
>
> other_tuple->iifidx = info.indev->ifindex;
> for (i = info.num_encaps - 1; i >= 0; i--) {
> other_tuple->encap[j].id = info.encap[i].id;
> other_tuple->encap[j].proto = info.encap[i].proto;
> if (info.ingress_vlans & BIT(i))
> other_tuple->in_vlan_ingress |= BIT(j);
> j++;
> }
> other_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;
>> +
>
> Here I use the skb to fill other_tuple->encaps. I understand you want to
> do this differently.
> Then I call nft_dev_fill_bridge_path() with !dir first, then dir.
>
>> + 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;
>
> This could move to nft_dev_fill_bridge_path() (only 1 line) as both
> tuples are also known there.
>
>> +
>> + 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:
>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support
2026-07-12 13:46 ` Eric Woudstra
@ 2026-07-12 19:13 ` Pablo Neira Ayuso
0 siblings, 0 replies; 12+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-12 19:13 UTC (permalink / raw)
To: Eric Woudstra; +Cc: netfilter-devel, razor, fw
On Sun, Jul 12, 2026 at 03:46:02PM +0200, Eric Woudstra wrote:
> On 7/12/26 11:27 AM, Eric Woudstra wrote:
> > On 7/10/26 12:07 PM, Pablo Neira Ayuso wrote:
[...]
> After fixing the problems in the patch-set mentioned earlier, when the sotware
> fastpath is setup correctly:
How are you testing this, my series is passing tests with netns and
veths with:
- plain bridge ports.
- VLAN devices used in the ports.
> Setting up a hardware offloaded flow, because the hardware supports it,
> this crashes:
>
> [ 283.380108] Unable to handle kernel execute from non-executable memory at virtual address 0000000000000000
> [ 283.389925] Mem abort info:
> [ 283.393178] ESR = 0x0000000086000004
> [ 283.396940] EC = 0x21: IABT (current EL), IL = 32 bits
> [ 283.402299] SET = 0, FnV = 0
> [ 283.405358] EA = 0, S1PTW = 0
> [ 283.408498] FSC = 0x04: level 0 translation fault
> [ 283.413473] user pgtable: 4k pages, 48-bit VAs, pgdp=00000000419d9000
> [ 283.419918] [0000000000000000] pgd=0000000000000000, p4d=0000000000000000
> [ 283.426766] Internal error: Oops: 0000000086000004 [#1] SMP
> [ 283.432432] Modules linked in: nft_flow_offload nf_flow_table_inet nf_flow_table nft_masq nft_chain_nat nf_nat nf_conntrack_bridge nft_ct nf_conntrack nf_defrag_ipv6 nf_defrag_ipv4 nf_tables cdc_mbim cdc_wdm cdc_ncm r8153_ecm cdc_ether usbnet r8152 mii mt7915e mt76_connac_lib mt76 mac80211 cfg80211 rfkill libarc4
> [ 283.460181] CPU: 1 UID: 0 PID: 2420 Comm: kworker/u16:6 Not tainted 7.2.0-rc1-bpirnn #12 PREEMPT
> [ 283.469040] Hardware name: Bananapi BPI-R3 (DT)
> [ 283.473558] Workqueue: nf_ft_offload_add flow_offload_work_handler [nf_flow_table]
> [ 283.481128] pstate: 60400005 (nZCv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--)
> [ 283.488084] pc : 0x0
> [ 283.490269] lr : nf_flow_offload_rule_alloc+0x45c/0x4b0 [nf_flow_table]
> [ 283.496875] sp : ffff8000830b3c90
> [ 283.500176] x29: ffff8000830b3c90 x28: ffff00000afc06b8 x27: ffff000007a8a180
> [ 283.507298] x26: ffff000006704000 x25: ffff000006704000 x24: ffff0000029598c8
> [ 283.514420] x23: ffff00000af7ac50 x22: 0000000000000000 x21: ffff000006704000
> [ 283.521541] x20: ffff000002959850 x19: ffff000002959800 x18: 0000000000000000
> [ 283.528662] x17: 0000000000000000 x16: 0000000000000000 x15: 0000000000000000
> [ 283.535785] x14: 0000000000000000 x13: 0000000000000008 x12: 0101010101010101
> [ 283.542914] x11: 7f7f7f7f7f7f7f7f x10: fefefefefefeff63 x9 : 0000000000000000
> [ 283.550038] x8 : ffff000001f25000 x7 : 0000000000000000 x6 : 000000000000003f
> [ 283.557159] x5 : 00000000ffffffff x4 : 0000000000000000 x3 : ffff000002959800
> [ 283.564280] x2 : 0000000000000000 x1 : ffff000006704000 x0 : ffff000007a8a180
> [ 283.571405] Call trace:
> [ 283.573841] 0x0 (P)
> [ 283.576022] flow_offload_work_handler+0x60/0x358 [nf_flow_table]
> [ 283.582111] process_scheduled_works+0x210/0x30c
> [ 283.586731] worker_thread+0x140/0x1d4
> [ 283.590478] kthread+0xf8/0x108
> [ 283.593617] ret_from_fork+0x10/0x20
>
> Adding .action = nf_flow_rule_bridge, with the function as it is in my latest
> patch named "netfilter: nf_flow_table_offload: Add nf_flow_rule_bridge()",
> it does not crash and the hardware offloaded path functions like a charm.
I still have to reintroduce the _unsupp chunk which it is not included
in the version.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support
2026-07-12 9:27 ` Eric Woudstra
2026-07-12 13:46 ` Eric Woudstra
@ 2026-07-12 19:19 ` Pablo Neira Ayuso
2026-07-13 8:24 ` Pablo Neira Ayuso
2 siblings, 0 replies; 12+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-12 19:19 UTC (permalink / raw)
To: Eric Woudstra; +Cc: netfilter-devel, razor, fw
On Sun, Jul 12, 2026 at 11:27:50AM +0200, Eric Woudstra wrote:
[...]
> On 7/10/26 12:07 PM, Pablo Neira Ayuso wrote:
> > diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
> > index 5455149e5d9a..a3aa9a9ce673 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,67 @@ 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 *dev,
> > + unsigned char *src_ha,
> > + unsigned char *dst_ha)
> > +{
> > + struct flow_offload_tuple *this_tuple = &flow->tuplehash[dir].tuple;
>
> Add:
>
> struct flow_offload_tuple *other_tuple = &flow->tuplehash[!dir].tuple;
>
> See below.
>
> > + 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);
> > +
>
> Here you could add the following to handle the encaps on this_tuple.
Why?
> for (i = this_tuple->encap_num - 1; i >= 0 ; i--) {
> if (info.num_encaps >= NF_FLOW_TABLE_ENCAP_MAX)
> return -1;
>
> if (this_tuple->in_vlan_ingress & BIT(i))
> continue;
I don't do bridge vlan filtering at this stage. I have to teach
nf_conntrack_bridge to track PPPoE/VLAN tagged packets, this is not
included in this series.
> info.encap[info.num_encaps].id = this_tuple->encap[i].id;
> info.encap[info.num_encaps].proto = this_tuple->encap[i].proto;
> info.num_encaps++;
>
> if (this_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 = this_tuple->encap[i].id;
> ctx.vlan[ctx.num_vlans].proto = this_tuple->encap[i].proto;
> ctx.num_vlans++;
> }
>
> > + if (dev_fill_forward_path(&ctx, &stack) < 0 ||
> > + nft_dev_path_info(&stack, &info, dst_ha, &ft->data) < 0)
> > + return -1;
> > +
> > + if (!nft_flowtable_find_dev(info.indev, ft))
> > + return -1;
> > +
>
> After replacing dev_fill_forward_path() with dev_fill_bridge_path(),
> from here...
>
> > + 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;
> > + j++;
> > + }
> > + this_tuple->encap_num = info.num_encaps;
>
> Until here, this_tuple needs to be the other_tuple.
> dev_fill_forward_path() does not traverse the bridge.
As I said, this series does not included bridge vlan filtering support.
> See other comment in other patch. Also, need to copy
> the in_vlan_ingress bit.
>
> So it becomes:
>
> other_tuple->iifidx = info.indev->ifindex;
> for (i = info.num_encaps - 1; i >= 0; i--) {
> other_tuple->encap[j].id = info.encap[i].id;
> other_tuple->encap[j].proto = info.encap[i].proto;
> if (info.ingress_vlans & BIT(i))
> other_tuple->in_vlan_ingress |= BIT(j);
> j++;
> }
> other_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;
> > +
>
> Here I use the skb to fill other_tuple->encaps. I understand you want to
> do this differently.
Using skb to populate tuples will not work, the skb comes with no tags
when VLAN/PPPoE devices are used in the bridge ports.
> Then I call nft_dev_fill_bridge_path() with !dir first, then dir.
>
> > + 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;
>
> This could move to nft_dev_fill_bridge_path() (only 1 line) as both
> tuples are also known there.
No, because other_tuple is unset when the first nft_dev_fill_bridge_path()
call is done.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path()
2026-07-12 9:28 ` Eric Woudstra
@ 2026-07-12 19:22 ` Pablo Neira Ayuso
0 siblings, 0 replies; 12+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-12 19:22 UTC (permalink / raw)
To: Eric Woudstra; +Cc: netfilter-devel, razor, fw
On Sun, Jul 12, 2026 at 11:28:11AM +0200, Eric Woudstra wrote:
> On 7/10/26 12:07 PM, Pablo Neira Ayuso wrote:
> > 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>
> > ---
> > v2: - move nft_dev_fill_forward_path_init() call after out: goto tag
> > to fix a crash otherwise in the existing flowtable ip family.
> >
> > include/linux/netdevice.h | 2 +-
> > net/core/dev.c | 18 +++++++-----------
> > net/netfilter/nf_flow_table_path.c | 14 ++++++++++++--
> > 3 files changed, 20 insertions(+), 14 deletions(-)
> >
> > diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
> > index 9981d637f8b5..db04b6d2e8d2 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 714d05283500..24c384ef9e78 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;
> > }
>
> Calling dev_fill_forward_path() from nft_dev_fill_bridge_path()
> it does not traverse the bridge. It exits at the source "indev"
> already. After calling nft_dev_path_info(), the info structure
> is filled in with the opposite device and the encaps of the
> opposite device.
Traversing the bridge is only needed for bridge vlan filtering and
that is not included in this initial support.
> Would need to add something like this:
>
> int dev_fill_bridge_path(struct net_device_path_ctx *ctx,
> struct net_device_path_stack *stack)
> {
> const struct net_device *last_dev, *br_dev;
> struct net_device_path *path;
>
> if (!ctx->dev || !netif_is_bridge_port(ctx->dev))
> return -1;
>
> br_dev = netdev_master_upper_dev_get_rcu((struct net_device *)ctx->dev);
> if (!br_dev || !br_dev->netdev_ops->ndo_fill_forward_path)
> return -1;
>
> last_dev = ctx->dev;
> path = dev_fwd_path(stack);
> if (!path)
> return -1;
>
> memset(path, 0, sizeof(struct net_device_path));
> if (br_dev->netdev_ops->ndo_fill_forward_path(ctx, path) < 0)
> return -1;
>
> if (!ctx->dev || WARN_ON_ONCE(last_dev == ctx->dev))
> return -1;
>
> return dev_fill_forward_path(ctx, stack);
> }
> EXPORT_SYMBOL_GPL(dev_fill_bridge_path);
>
> First need to find the bridge device to call ndo_fill_forward_path()
> from it.
>
> This also needs the patch "bridge: Add filling forward path from port to
> port"
> That patch still needs a change according to Nikolay.
Once bridge vlan filtering is added, but that need nf_conntrack_bridge
support for VLAN/PPPoE as I said.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support
2026-07-12 9:27 ` Eric Woudstra
2026-07-12 13:46 ` Eric Woudstra
2026-07-12 19:19 ` Pablo Neira Ayuso
@ 2026-07-13 8:24 ` Pablo Neira Ayuso
2 siblings, 0 replies; 12+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-13 8:24 UTC (permalink / raw)
To: Eric Woudstra; +Cc: netfilter-devel, razor, fw
On Sun, Jul 12, 2026 at 11:27:50AM +0200, Eric Woudstra wrote:
> On 7/10/26 12:07 PM, Pablo Neira Ayuso wrote:
[...]
> > + 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;
> > + j++;
> > + }
> > + this_tuple->encap_num = info.num_encaps;
nft_dev_fill_bridge_path() is called with indev for dir, but
dev_fill_forward() obtains the list of devices from indev. This reverse
iteration gives us the expected encapsulation before such list of
devices for this direction in the ingress path.
> Until here, this_tuple needs to be the other_tuple.
> dev_fill_forward_path() does not traverse the bridge.
> See other comment in other patch. Also, need to copy
> the in_vlan_ingress bit.
>
> So it becomes:
>
> other_tuple->iifidx = info.indev->ifindex;
> for (i = info.num_encaps - 1; i >= 0; i--) {
> other_tuple->encap[j].id = info.encap[i].id;
> other_tuple->encap[j].proto = info.encap[i].proto;
> if (info.ingress_vlans & BIT(i))
> other_tuple->in_vlan_ingress |= BIT(j);
> j++;
> }
> other_tuple->encap_num = info.num_encaps;
I don't see how it can be this way.
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-07-13 8:24 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-10 10:07 [PATCH nf-next,v2 0/3] initial flowtable bridge support Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 1/3] net: pass net_device_path_ctx struct to dev_fill_forward_path() Pablo Neira Ayuso
2026-07-11 9:30 ` Eric Woudstra
2026-07-12 9:28 ` Eric Woudstra
2026-07-12 19:22 ` Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 2/3] net: expose dev_fwd_path() helper via static inline Pablo Neira Ayuso
2026-07-10 10:07 ` [PATCH nf-next,v2 3/3] netfilter: flowtable: initial bridge support Pablo Neira Ayuso
2026-07-12 9:27 ` Eric Woudstra
2026-07-12 13:46 ` Eric Woudstra
2026-07-12 19:13 ` Pablo Neira Ayuso
2026-07-12 19:19 ` Pablo Neira Ayuso
2026-07-13 8:24 ` Pablo Neira Ayuso
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.