* [PATCH nf-next v8 0/2] netfilter: flowtable: tear down bridged flows on layer 2 roaming
@ 2026-10-17 23:58 Julius Bairaktaris
2026-10-17 23:58 ` [PATCH nf-next v8 1/2] netfilter: flowtable: tear down direct xmit flows when the fdb entry moves Julius Bairaktaris
2026-10-17 23:58 ` [PATCH nf-next v8 2/2] selftests: netfilter: nft_flowtable.sh: roam a host between two bridge ports Julius Bairaktaris
0 siblings, 2 replies; 4+ messages in thread
From: Julius Bairaktaris @ 2026-10-17 23:58 UTC (permalink / raw)
To: netfilter-devel
Cc: pablo, fw, kadlec, phil, horms, davem, edumazet, kuba, pabeni,
shuah, razor, idosch, ericwouds, dqfext, netdev, coreteam,
linux-kselftest, bridge, linux-kernel
A flowtable flow that transmits straight to a bridge port keeps sending
to the old port when the host moves to another port, for example a
Wi-Fi station roaming between access points.
Patch 1 makes the gc tear such a flow down, using the bridge fdb lookup
from Pablo's patch. Patch 2 adds selftest cases.
Tested on nf-next a5e7d8e446af in an x86 guest with PROVE_LOCKING:
nft_flowtable.sh passes, including all eleven roaming cases, with no
lockdep splat. A kprobe on flow_offload_teardown sees no teardown of
the test flow during 10 s of steady traffic in any case. struct
flow_offload stays at 296 bytes on x86_64.
Changes in v8:
- drop bridge_ifidx and bridge_vid (Pablo): the bridge port is found
from out.ifidx through the vlan devices the forward path has below
the port (new port_vlans field), the vid comes from the encap stack
(new bridge_vlan bit) or is the pvid of the port for an untagged port
of a vlan_filtering bridge
- move the fdb check into nf_flow_dst_valid() next to
nf_flow_dst_check(), and name the hook fdb_valid_dst() again (Pablo);
it now takes the bridge port device instead of net and ifindex
- drop the comments Pablo pointed out
- selftest: vlan_filtering with tagged plain and vlan ports, the router
address on a vlan device on the bridge, and vlan ports whose real
devices are ports of another bridge; no forward chain packet is
allowed once the flow is offloaded again
v7: https://lore.kernel.org/netfilter-devel/20261014235628.197976-1-julius@bairaktaris.de/
v6: https://lore.kernel.org/netfilter-devel/20261004171605.3544792-1-julius@bairaktaris.de/
Pablo's patch: https://lore.kernel.org/netfilter-devel/20261014210256.1573072-1-pablo@netfilter.org/
Julius Bairaktaris (2):
netfilter: flowtable: tear down direct xmit flows when the fdb entry
moves
selftests: netfilter: nft_flowtable.sh: roam a host between two bridge
ports
include/linux/netfilter.h | 7 +
include/net/netfilter/nf_flow_table.h | 8 +-
net/bridge/Makefile | 2 +
net/bridge/br.c | 2 +
net/bridge/br_nf_utils.c | 38 +++
net/bridge/br_private.h | 13 +
net/netfilter/core.c | 3 +
net/netfilter/nf_flow_table_core.c | 45 +++-
net/netfilter/nf_flow_table_path.c | 19 +-
.../selftests/net/netfilter/nft_flowtable.sh | 239 ++++++++++++++++++
10 files changed, 371 insertions(+), 5 deletions(-)
create mode 100644 net/bridge/br_nf_utils.c
base-commit: a5e7d8e446af9803e37a3b6a4d416fb41178348f
--
2.53.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH nf-next v8 1/2] netfilter: flowtable: tear down direct xmit flows when the fdb entry moves
2026-10-17 23:58 [PATCH nf-next v8 0/2] netfilter: flowtable: tear down bridged flows on layer 2 roaming Julius Bairaktaris
@ 2026-10-17 23:58 ` Julius Bairaktaris
2026-10-24 14:13 ` Julius Bairaktaris
2026-10-17 23:58 ` [PATCH nf-next v8 2/2] selftests: netfilter: nft_flowtable.sh: roam a host between two bridge ports Julius Bairaktaris
1 sibling, 1 reply; 4+ messages in thread
From: Julius Bairaktaris @ 2026-10-17 23:58 UTC (permalink / raw)
To: netfilter-devel
Cc: pablo, fw, kadlec, phil, horms, davem, edumazet, kuba, pabeni,
shuah, razor, idosch, ericwouds, dqfext, netdev, coreteam,
linux-kselftest, bridge, linux-kernel
A direct xmit flow that reaches a bridge port keeps sending to that port
when the host moves to another port of the bridge, and packets from the
other side keep the flow from timing out.
The gc now checks the fdb entry next to the dst check, through
nf_br_utils_hook, and tears the flow down when the entry points to
another port. An entry that aged out does not tear it down, because
offloaded packets do not refresh it.
The bridge port is found from out.ifidx through the vlan devices that
the forward path has below the port. The vid is the bridge vlan in the
encap stack, or the pvid of the port for an untagged port of a
vlan_filtering bridge.
Reported-by: Eric Woudstra <ericwouds@gmail.com>
Link: https://lore.kernel.org/netfilter-devel/20250925182623.114045-1-ericwouds@gmail.com/
Co-developed-by: Pablo Neira Ayuso <pablo@netfilter.org>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
---
Assisted-by covers my changes on top of Pablo's patch.
include/linux/netfilter.h | 7 +++++
include/net/netfilter/nf_flow_table.h | 8 ++++-
net/bridge/Makefile | 2 ++
net/bridge/br.c | 2 ++
net/bridge/br_nf_utils.c | 38 ++++++++++++++++++++++
net/bridge/br_private.h | 13 ++++++++
net/netfilter/core.c | 3 ++
net/netfilter/nf_flow_table_core.c | 45 +++++++++++++++++++++++++--
net/netfilter/nf_flow_table_path.c | 19 +++++++++--
9 files changed, 132 insertions(+), 5 deletions(-)
create mode 100644 net/bridge/br_nf_utils.c
diff --git a/include/linux/netfilter.h b/include/linux/netfilter.h
index e99afc1414cd3..be85d8f68b452 100644
--- a/include/linux/netfilter.h
+++ b/include/linux/netfilter.h
@@ -67,6 +67,13 @@ static inline void nf_inet_addr_mask(const union nf_inet_addr *a1,
#endif
}
+struct nf_br_utils {
+ bool (*fdb_valid_dst)(const struct net_device *dev,
+ const unsigned char *addr, u16 vid);
+};
+
+extern const struct nf_br_utils __rcu *nf_br_utils_hook;
+
int netfilter_init(void);
struct sk_buff;
diff --git a/include/net/netfilter/nf_flow_table.h b/include/net/netfilter/nf_flow_table.h
index 5b611efaa3cd1..6865b542eb3be 100644
--- a/include/net/netfilter/nf_flow_table.h
+++ b/include/net/netfilter/nf_flow_table.h
@@ -152,6 +152,9 @@ struct flow_offload_tuple {
xmit_type:3,
encap_num:2,
needs_gso_segment:1,
+ bridge_path:1,
+ bridge_vlan:1,
+ port_vlans:2,
tun_num:2,
in_vlan_ingress:2;
u16 mtu;
@@ -234,7 +237,10 @@ struct nf_flow_route {
u32 hw_ifindex;
u8 h_source[ETH_ALEN];
u8 h_dest[ETH_ALEN];
- u8 needs_gso_segment:1;
+ u8 needs_gso_segment:1,
+ bridge_path:1,
+ bridge_vlan:1,
+ port_vlans:2;
} out;
enum flow_offload_xmit_type xmit_type;
} tuple[FLOW_OFFLOAD_DIR_MAX];
diff --git a/net/bridge/Makefile b/net/bridge/Makefile
index 1203dc19e15c9..fc53c332246de 100644
--- a/net/bridge/Makefile
+++ b/net/bridge/Makefile
@@ -14,6 +14,8 @@ bridge-$(CONFIG_SYSFS) += br_sysfs_if.o br_sysfs_br.o
bridge-$(subst m,y,$(CONFIG_BRIDGE_NETFILTER)) += br_nf_core.o
+bridge-$(subst m,y,$(CONFIG_NF_FLOW_TABLE)) += br_nf_utils.o
+
br_netfilter-y := br_netfilter_hooks.o
br_netfilter-$(subst m,y,$(CONFIG_IPV6)) += br_netfilter_ipv6.o
obj-$(CONFIG_BRIDGE_NETFILTER) += br_netfilter.o
diff --git a/net/bridge/br.c b/net/bridge/br.c
index d9a39ff98868d..c0618ef6dac5c 100644
--- a/net/bridge/br.c
+++ b/net/bridge/br.c
@@ -458,6 +458,7 @@ static int __init br_init(void)
goto err_out6;
brioctl_set(br_ioctl_stub);
+ br_nf_utils_init();
#if IS_MODULE(CONFIG_BRIDGE_NETFILTER)
pr_info("bridge: filtering via arp/ip/ip6tables is no longer available "
@@ -486,6 +487,7 @@ static int __init br_init(void)
static void __exit br_deinit(void)
{
+ br_nf_utils_fini();
stp_proto_unregister(&br_stp_proto);
br_netlink_fini();
unregister_switchdev_blocking_notifier(&br_switchdev_blocking_notifier);
diff --git a/net/bridge/br_nf_utils.c b/net/bridge/br_nf_utils.c
new file mode 100644
index 0000000000000..300bce6c1b0b8
--- /dev/null
+++ b/net/bridge/br_nf_utils.c
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/netdevice.h>
+#include <linux/netfilter.h>
+
+#include "br_private.h"
+
+static bool nf_br_fdb_valid_dst(const struct net_device *dev,
+ const unsigned char *addr, u16 vid)
+{
+ const struct net_bridge_fdb_entry *f;
+ const struct net_bridge_port *p;
+
+ p = br_port_get_check_rcu(dev);
+ if (!p)
+ return false;
+
+ if (!vid && br_opt_get(p->br, BROPT_VLAN_ENABLED))
+ vid = br_get_pvid(nbp_vlan_group_rcu(p));
+
+ /* Offloaded packets do not refresh the entry, it can age out. */
+ f = br_fdb_find_rcu(p->br, addr, vid);
+
+ return !f || br_fdb_dst_port(f) == p;
+}
+
+static const struct nf_br_utils nf_br_utils = {
+ .fdb_valid_dst = nf_br_fdb_valid_dst,
+};
+
+void br_nf_utils_init(void)
+{
+ rcu_assign_pointer(nf_br_utils_hook, &nf_br_utils);
+}
+
+void br_nf_utils_fini(void)
+{
+ RCU_INIT_POINTER(nf_br_utils_hook, NULL);
+}
diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h
index 05921192ebda1..22c9e17d76175 100644
--- a/net/bridge/br_private.h
+++ b/net/bridge/br_private.h
@@ -2164,6 +2164,19 @@ struct nf_br_ops {
};
extern const struct nf_br_ops __rcu *nf_br_ops;
+#if IS_ENABLED(CONFIG_NF_FLOW_TABLE)
+void br_nf_utils_init(void);
+void br_nf_utils_fini(void);
+#else
+static inline void br_nf_utils_init(void)
+{
+}
+
+static inline void br_nf_utils_fini(void)
+{
+}
+#endif
+
/* br_netfilter.c */
#if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
int br_nf_core_init(void);
diff --git a/net/netfilter/core.c b/net/netfilter/core.c
index 940dea2663e95..914f0934f6da3 100644
--- a/net/netfilter/core.c
+++ b/net/netfilter/core.c
@@ -814,6 +814,9 @@ static struct pernet_operations netfilter_net_ops = {
.exit = netfilter_net_exit,
};
+const struct nf_br_utils __rcu *nf_br_utils_hook;
+EXPORT_SYMBOL_GPL(nf_br_utils_hook);
+
int __init netfilter_init(void)
{
int ret;
diff --git a/net/netfilter/nf_flow_table_core.c b/net/netfilter/nf_flow_table_core.c
index 0d72dc8ae47a1..113580adf10d3 100644
--- a/net/netfilter/nf_flow_table_core.c
+++ b/net/netfilter/nf_flow_table_core.c
@@ -5,6 +5,7 @@
#include <linux/netfilter.h>
#include <linux/rhashtable.h>
#include <linux/netdevice.h>
+#include <linux/if_vlan.h>
#include <net/ip.h>
#include <net/ip6_route.h>
#include <net/netfilter/nf_tables.h>
@@ -139,6 +140,9 @@ static int flow_offload_fill_route(struct flow_offload *flow,
memcpy(flow_tuple->out.h_source, route->tuple[dir].out.h_source,
ETH_ALEN);
flow_tuple->out.ifidx = route->tuple[dir].out.ifindex;
+ flow_tuple->bridge_path = route->tuple[dir].out.bridge_path;
+ flow_tuple->bridge_vlan = route->tuple[dir].out.bridge_vlan;
+ flow_tuple->port_vlans = route->tuple[dir].out.port_vlans;
break;
case FLOW_OFFLOAD_XMIT_XFRM:
case FLOW_OFFLOAD_XMIT_NEIGH:
@@ -572,6 +576,43 @@ static void nf_flow_table_extend_ct_timeout(struct nf_conn *ct)
nf_ct_put(ct);
}
+static bool nf_flow_dst_valid(struct nf_flowtable *flow_table,
+ struct flow_offload *flow, int dir)
+{
+ struct flow_offload_tuple *other = &flow->tuplehash[!dir].tuple;
+ struct flow_offload_tuple *tuple = &flow->tuplehash[dir].tuple;
+ struct net *net = read_pnet(&flow_table->net);
+ const struct nf_br_utils *br_utils;
+ struct net_device *dev;
+ bool valid = true;
+ u16 vid = 0;
+ int i;
+
+ if (!nf_flow_dst_check(tuple))
+ return false;
+
+ if (!tuple->bridge_path)
+ return true;
+
+ rcu_read_lock();
+ br_utils = rcu_dereference(nf_br_utils_hook);
+ if (!br_utils)
+ goto out;
+
+ dev = dev_get_by_index_rcu(net, tuple->out.ifidx);
+ for (i = 0; dev && i < tuple->port_vlans; i++)
+ dev = __vlan_find_dev_deep_rcu(dev, other->encap[i].proto,
+ other->encap[i].id);
+ if (tuple->bridge_vlan)
+ vid = other->encap[i].id;
+
+ valid = dev && br_utils->fdb_valid_dst(dev, tuple->out.h_dest, vid);
+out:
+ rcu_read_unlock();
+
+ return valid;
+}
+
static void nf_flow_offload_gc_step(struct nf_flowtable *flow_table,
struct flow_offload *flow, void *data)
{
@@ -584,8 +625,8 @@ static void nf_flow_offload_gc_step(struct nf_flowtable *flow_table,
if (nf_flow_has_expired(flow) ||
nf_ct_is_dying(flow->ct) ||
- !nf_flow_dst_check(&flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple) ||
- !nf_flow_dst_check(&flow->tuplehash[FLOW_OFFLOAD_DIR_REPLY].tuple) ||
+ !nf_flow_dst_valid(flow_table, flow, FLOW_OFFLOAD_DIR_ORIGINAL) ||
+ !nf_flow_dst_valid(flow_table, flow, FLOW_OFFLOAD_DIR_REPLY) ||
nf_flow_custom_gc(flow_table, flow)) {
flow_offload_teardown(flow);
teardown = true;
diff --git a/net/netfilter/nf_flow_table_path.c b/net/netfilter/nf_flow_table_path.c
index d90013685bf12..779d1fc0ed994 100644
--- a/net/netfilter/nf_flow_table_path.c
+++ b/net/netfilter/nf_flow_table_path.c
@@ -95,7 +95,10 @@ struct nft_forward_info {
u8 ingress_vlans;
u8 h_source[ETH_ALEN];
u8 h_dest[ETH_ALEN];
- bool needs_gso_segment;
+ u8 needs_gso_segment:1,
+ bridge_path:1,
+ bridge_vlan:1,
+ port_vlans:2;
enum flow_offload_xmit_type xmit_type;
};
@@ -107,7 +110,7 @@ static int nft_dev_path_info(struct net_device_path_stack *stack,
unsigned char *ha, struct nft_flowtable *ft)
{
const struct net_device_path *path;
- int i;
+ int i, n;
memcpy(info->h_dest, ha, ETH_ALEN);
@@ -148,6 +151,8 @@ static int nft_dev_path_info(struct net_device_path_stack *stack,
info->encap[info->num_encaps].proto =
path->encap.proto;
info->num_encaps++;
+ if (info->bridge_path)
+ info->port_vlans++;
}
if (path->type == DEV_PATH_PPPOE) {
memcpy(info->h_dest, path->encap.h_dest, ETH_ALEN);
@@ -183,6 +188,13 @@ static int nft_dev_path_info(struct net_device_path_stack *stack,
case DEV_PATH_BR_VLAN_KEEP:
break;
}
+ info->bridge_path = 1;
+ n = info->num_encaps;
+ info->bridge_vlan = path->bridge.vlan_id && n &&
+ info->encap[n - 1].id ==
+ path->bridge.vlan_id &&
+ info->encap[n - 1].proto ==
+ path->bridge.vlan_proto;
info->xmit_type = FLOW_OFFLOAD_XMIT_DIRECT;
break;
default:
@@ -260,6 +272,9 @@ static int nft_dev_forward_path(const struct nft_pktinfo *pkt,
if (info.xmit_type == FLOW_OFFLOAD_XMIT_DIRECT) {
memcpy(route->tuple[dir].out.h_source, info.h_source, ETH_ALEN);
memcpy(route->tuple[dir].out.h_dest, info.h_dest, ETH_ALEN);
+ route->tuple[dir].out.bridge_path = info.bridge_path;
+ route->tuple[dir].out.bridge_vlan = info.bridge_vlan;
+ route->tuple[dir].out.port_vlans = info.port_vlans;
route->tuple[dir].xmit_type = info.xmit_type;
}
route->tuple[dir].out.needs_gso_segment = info.needs_gso_segment;
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH nf-next v8 2/2] selftests: netfilter: nft_flowtable.sh: roam a host between two bridge ports
2026-10-17 23:58 [PATCH nf-next v8 0/2] netfilter: flowtable: tear down bridged flows on layer 2 roaming Julius Bairaktaris
2026-10-17 23:58 ` [PATCH nf-next v8 1/2] netfilter: flowtable: tear down direct xmit flows when the fdb entry moves Julius Bairaktaris
@ 2026-10-17 23:58 ` Julius Bairaktaris
1 sibling, 0 replies; 4+ messages in thread
From: Julius Bairaktaris @ 2026-10-17 23:58 UTC (permalink / raw)
To: netfilter-devel
Cc: pablo, fw, kadlec, phil, horms, davem, edumazet, kuba, pabeni,
shuah, razor, idosch, ericwouds, dqfext, netdev, coreteam,
linux-kselftest, bridge, linux-kernel
Move a host to another port of a bridge on the router while its flow is
offloaded, and check that the transfer continues and is offloaded again.
The cases are plain and vlan bridge ports, vlan ports whose real devices
are ports of another bridge, "bridge fdb replace", a pinned and then
deleted fdb entry, an aged out entry (the flow must stay), the plain
case over IPv6, and a vlan_filtering bridge with untagged ports, with
tagged plain and vlan ports, and with the router address on a vlan
device on the bridge.
Without the gc check the transfer stalls when the host moves.
Assisted-by: Claude:claude-fable-5-1
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
---
.../selftests/net/netfilter/nft_flowtable.sh | 239 ++++++++++++++++++
1 file changed, 239 insertions(+)
diff --git a/tools/testing/selftests/net/netfilter/nft_flowtable.sh b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
index 449c518bd947d..39b764d03a0b1 100755
--- a/tools/testing/selftests/net/netfilter/nft_flowtable.sh
+++ b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
@@ -851,6 +851,245 @@ test_ipip
test_bridge
+# Roam a host between two ports of a bridge on the router while its flow is
+# offloaded, as a station moving between two access points attached to ports
+# of one bridge. The host keeps its address and mac on a bridge of its own
+# and roams by moving that bridge's port, so only the fdb changes. With a vid
+# the bridge ports are vlan devices, whose ifindex differs from the device
+# the flow transmits on. With "replace" the router's fdb entry is sticky and
+# the move is a "bridge fdb replace". With "del" the host stays, its entry is
+# pinned to the other port and then deleted, and a ping relearns it. With
+# "age" the host stays and its entry ages out, which must not tear the flow
+# down. A flow that stays on the fast path leaves no more than a few packets
+# in the forward chain, the counter tells offloaded from slow path. The
+# plain roam also runs over IPv6, and on a vlan_filtering bridge: with
+# untagged ports ("vf"), whose fdb entries are keyed by the pvid of the
+# port, with tagged ports ("tag"), and with untagged ports and the router
+# address on a vlan device on the bridge ("upper").
+test_bridge_roam()
+{
+ local vid=$1 how=$2 fam=${3:-4} vf=$4 trunk=$5
+ local p0=sr0 p1=sr1 r0=rr0 r1=rr1 rbr=br0
+ local mac=02:00:00:09:01:99
+ local what="roaming on bridge${vf:+ with vlan_filtering}"
+ [ "$vf" = tag ] && what="$what and tagged ports"
+ [ "$vf" = upper ] && what="$what and a vlan device on the bridge"
+ what="$what${vid:+ with vlan ports}${how:+ by fdb $how}"
+ what="$what${trunk:+ whose real devices are ports of another bridge}"
+ local hn=10.9.1 wn=10.9.2 dot=. len=24 nodad="" ip6="" srv=10.9.2.99
+ local min=10000000
+ local dev old new slow fdb lpid cpid win=2
+
+ if [ "$fam" = 6 ]; then
+ hn=dead:9:1 wn=dead:9:2 dot=:: len=64 nodad=nodad ip6=-6
+ srv="[dead:9:2::99]"
+ what="$what over IPv6"
+ fi
+
+ setup_ns rns1 rnsr rns2
+
+ if ! ip -net "$rnsr" link add br0 type bridge ${vf:+vlan_filtering 1} 2>/dev/null; then
+ echo "SKIP: could not add bridge for $what"
+ [ "$ret" -eq 0 ] && ret=$ksft_skip
+ return
+ fi
+
+ ip link add sr0 netns "$rns1" type veth peer name rr0 netns "$rnsr"
+ ip link add sr1 netns "$rns1" type veth peer name rr1 netns "$rnsr"
+ ip link add rw0 netns "$rnsr" type veth peer name w0 netns "$rns2"
+ for dev in sr0 sr1; do
+ ip -net "$rns1" link set "$dev" up
+ done
+ for dev in rr0 rr1; do
+ ip -net "$rnsr" link set "$dev" up
+ done
+
+ if [ -n "$vid" ]; then
+ for dev in sr0 sr1; do
+ ip -net "$rns1" link add link "$dev" name "$dev.$vid" type vlan id "$vid"
+ ip -net "$rns1" link set "$dev.$vid" up
+ done
+ for dev in rr0 rr1; do
+ ip -net "$rnsr" link add link "$dev" name "$dev.$vid" type vlan id "$vid"
+ ip -net "$rnsr" link set "$dev.$vid" up
+ done
+ p0=sr0.$vid; p1=sr1.$vid; r0=rr0.$vid; r1=rr1.$vid
+ fi
+ if [ -n "$trunk" ]; then
+ ip -net "$rnsr" link add br1 type bridge
+ ip -net "$rnsr" link set rr0 master br1
+ ip -net "$rnsr" link set rr1 master br1
+ ip -net "$rnsr" link set br1 up
+ fi
+
+ ip -net "$rnsr" link set "$r0" master br0
+ ip -net "$rnsr" link set "$r1" master br0
+ ip -net "$rnsr" link set br0 up
+ if [ "$vf" = tag ]; then
+ for dev in "$r0" "$r1"; do
+ bridge -n "$rnsr" vlan add vid 10 dev "$dev"
+ done
+ bridge -n "$rnsr" vlan add vid 10 dev br0 self pvid untagged
+ for dev in "$p0" "$p1"; do
+ ip -net "$rns1" link add link "$dev" name "$dev.10" type vlan id 10
+ ip -net "$rns1" link set "$dev.10" up
+ done
+ p0=$p0.10; p1=$p1.10
+ elif [ "$vf" = upper ]; then
+ for dev in "$r0" "$r1"; do
+ bridge -n "$rnsr" vlan add vid 10 dev "$dev" pvid untagged
+ done
+ bridge -n "$rnsr" vlan add vid 10 dev br0 self
+ ip -net "$rnsr" link add link br0 name br0.10 type vlan id 10
+ ip -net "$rnsr" link set br0.10 up
+ rbr=br0.10
+ fi
+ ip -net "$rnsr" addr add $hn${dot}1/$len dev $rbr $nodad
+ ip -net "$rnsr" addr add $wn${dot}1/$len dev rw0 $nodad
+ ip -net "$rnsr" link set rw0 up
+ ip netns exec "$rnsr" sysctl -q net.ipv4.ip_forward=1
+ ip netns exec "$rnsr" sysctl -q net.ipv6.conf.all.forwarding=1
+
+ ip -net "$rns1" link add stbr type bridge
+ ip -net "$rns1" link set stbr address $mac
+ ip -net "$rns1" link set "$p0" master stbr
+ ip -net "$rns1" link set stbr up
+ ip -net "$rns1" addr add $hn${dot}99/$len dev stbr $nodad
+ ip $ip6 -net "$rns1" route add default via $hn${dot}1
+
+ ip -net "$rns2" addr add $wn${dot}99/$len dev w0 $nodad
+ ip -net "$rns2" link set w0 up
+ ip $ip6 -net "$rns2" route add default via $wn${dot}1
+
+ # no neighbour probes, so the host only sends when it receives
+ ip -net "$rnsr" neigh replace $hn${dot}99 lladdr $mac nud permanent dev $rbr
+ ip -net "$rns1" neigh replace $hn${dot}1 nud permanent dev stbr \
+ lladdr "$(ip -net "$rnsr" -br link show br0 | awk '{print $3}')"
+
+ if ! ip netns exec "$rnsr" nft -f - <<EOF
+table inet filter {
+ counter roam_repl { }
+ flowtable f1 { hook ingress priority 0; devices = { rr0, rr1, rw0 }; }
+ chain forward {
+ type filter hook forward priority 0; policy accept;
+ meta oif "rw0" tcp dport 12345 flow add @f1
+ meta iif "rw0" tcp sport 12345 counter name roam_repl flow add @f1
+ }
+}
+EOF
+ then
+ echo "SKIP: could not load ruleset for $what"
+ [ "$ret" -eq 0 ] && ret=$ksft_skip
+ return
+ fi
+
+ # the host discards what it receives, its socket byte counter is read
+ rx_bytes() { ip netns exec "$rns1" ss -tin "dst $srv" 2>/dev/null |
+ grep -o 'bytes_received:[0-9]*' | head -1 | cut -d: -f2; }
+ rx_started() { local n; n=$(rx_bytes); [ "${n:-0}" -gt 0 ]; }
+ # forward chain packets of the reply direction since the last call
+ slow_pkts() { ip netns exec "$rnsr" nft reset counter inet filter roam_repl |
+ grep -o 'packets [0-9]*' | cut -d' ' -f2; }
+ roam_listener_ready() { ss -N "$rns2" -lnt -o "sport = :12345" | grep -q 12345; }
+
+ roam_stop() { kill "$cpid" "$lpid" 2>/dev/null; wait "$cpid" "$lpid" 2>/dev/null; }
+
+ timeout "$SOCAT_TIMEOUT" ip netns exec "$rns2" socat -u \
+ OPEN:/dev/zero TCP$fam-LISTEN:12345,reuseaddr &
+ lpid=$!
+ if ! busywait "$BUSYWAIT_TIMEOUT" roam_listener_ready; then
+ echo "FAIL: $what: listener did not start" 1>&2
+ ret=1
+ kill "$lpid" 2>/dev/null; wait "$lpid" 2>/dev/null
+ return
+ fi
+ timeout "$SOCAT_TIMEOUT" ip netns exec "$rns1" socat -u \
+ TCP$fam:$srv:12345 OPEN:/dev/null &
+ cpid=$!
+
+ if ! busywait "$BUSYWAIT_TIMEOUT" rx_started; then
+ echo "FAIL: $what: transfer did not start" 1>&2
+ ret=1
+ roam_stop
+ return
+ fi
+ sleep 2
+ slow_pkts > /dev/null
+ old=$(rx_bytes)
+ sleep 2
+ new=$(rx_bytes)
+ slow=$(slow_pkts)
+ if [ -z "$slow" ] || [ $((${new:-0} - ${old:-0})) -lt $min ] ||
+ [ "$slow" -gt 100 ]; then
+ echo "FAIL: $what: not offloaded before the roam:" \
+ "$((${new:-0} - ${old:-0})) bytes, ${slow:-no} forward chain packets" 1>&2
+ ret=1
+ roam_stop
+ return
+ fi
+
+ if [ "$how" = age ]; then
+ # ageing_time is in centiseconds
+ ip -net "$rnsr" link set br0 type bridge ageing_time 200
+ win=10
+ elif [ "$how" = del ]; then
+ bridge -n "$rnsr" fdb replace $mac dev "$r1" master static sticky
+ sleep 2
+ bridge -n "$rnsr" fdb del $mac dev "$r1" master static
+ ip netns exec "$rns1" ping $ip6 -c 2 -W 1 -q $hn${dot}1 >/dev/null 2>&1
+ else
+ # roam: move the host bridge to the other port and send on it
+ if [ "$how" = replace ]; then
+ bridge -n "$rnsr" fdb replace $mac dev "$r0" master static sticky
+ fi
+ ip -net "$rns1" link set "$p0" nomaster
+ ip -net "$rns1" link set "$p1" master stbr
+ ip netns exec "$rns1" ping $ip6 -c 2 -W 1 -q $hn${dot}1 >/dev/null 2>&1
+ if [ "$how" = replace ]; then
+ bridge -n "$rnsr" fdb replace $mac dev "$r1" master static sticky
+ fi
+ fi
+
+ # the bytes in flight at the roam drain within the first seconds; a
+ # stale flow delivers nothing after that, a torn down one resumes and
+ # is offloaded again.
+ sleep 4
+ old=$(rx_bytes)
+ sleep 4
+ new=$(rx_bytes)
+ slow_pkts > /dev/null
+ sleep $win
+ slow=$(slow_pkts)
+ # before the host's FIN passes the bridge and relearns the entry
+ fdb=$(bridge -n "$rnsr" fdb show br br0 | grep "$mac.*master")
+ roam_stop
+
+ if [ "$how" = age ] && [ -n "$fdb" ]; then
+ echo "FAIL: $what: the host's fdb entry did not age out" 1>&2
+ ret=1
+ elif [ $((${new:-0} - ${old:-0})) -lt $min ]; then
+ echo "FAIL: $what: transfer stalled after the roam" 1>&2
+ ret=1
+ elif [ "$slow" != 0 ]; then
+ echo "FAIL: $what: $slow forward chain packets after the roam" 1>&2
+ ret=1
+ else
+ echo "PASS: flow offload for $what"
+ fi
+}
+
+test_bridge_roam ""
+test_bridge_roam 100
+test_bridge_roam "" replace
+test_bridge_roam "" del
+test_bridge_roam "" age
+test_bridge_roam "" "" 6
+test_bridge_roam "" "" 4 vf
+test_bridge_roam "" "" 4 tag
+test_bridge_roam 100 "" 4 tag
+test_bridge_roam 100 "" 4 "" trunk
+test_bridge_roam "" "" 4 upper
+
KEY_SHA="0x"$(ps -af | sha1sum | cut -d " " -f 1)
KEY_AES="0x"$(ps -af | md5sum | cut -d " " -f 1)
SPI1=$RANDOM
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH nf-next v8 1/2] netfilter: flowtable: tear down direct xmit flows when the fdb entry moves
2026-10-17 23:58 ` [PATCH nf-next v8 1/2] netfilter: flowtable: tear down direct xmit flows when the fdb entry moves Julius Bairaktaris
@ 2026-10-24 14:13 ` Julius Bairaktaris
0 siblings, 0 replies; 4+ messages in thread
From: Julius Bairaktaris @ 2026-10-24 14:13 UTC (permalink / raw)
To: netfilter-devel
Cc: Pablo Neira Ayuso, Florian Westphal, kadlec, phil, horms, davem,
edumazet, kuba, pabeni, shuah, Nikolay Aleksandrov, idosch,
Eric Woudstra, dqfext, netdev, coreteam, linux-kselftest, bridge,
linux-kernel
Hi Pablo,
Please do not apply v8. Sashiko found an out-of-bounds read in it: with
a vlan device of one bridge enslaved to another bridge, port_vlans counts
the vlans below both bridges, so the gc looks up the bridge port from the
wrong device and reads past encap[]. Resetting port_vlans at each bridge
fixes it, and a selftest case covers it.
v9 has that fix, credits your patch as its author with your sign-off
and a note of my changes instead of Co-developed-by, and is rebased
onto nf-next. I will post it once nf-next reopens.
pw-bot: changes-requested
Julius
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-24 14:13 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-17 23:58 [PATCH nf-next v8 0/2] netfilter: flowtable: tear down bridged flows on layer 2 roaming Julius Bairaktaris
2026-10-17 23:58 ` [PATCH nf-next v8 1/2] netfilter: flowtable: tear down direct xmit flows when the fdb entry moves Julius Bairaktaris
2026-10-24 14:13 ` Julius Bairaktaris
2026-10-17 23:58 ` [PATCH nf-next v8 2/2] selftests: netfilter: nft_flowtable.sh: roam a host between two bridge ports Julius Bairaktaris
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox