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, "bridge fdb replace", a
pinned and then deleted fdb entry, an aged out entry (the flow must
stay), the plain case over IPv6 and on a vlan_filtering 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 <[email protected]>
---
 .../selftests/net/netfilter/nft_flowtable.sh  | 204 ++++++++++++++++++
 1 file changed, 204 insertions(+)

diff --git a/tools/testing/selftests/net/netfilter/nft_flowtable.sh 
b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
index 449c518bd947d..e6b345ff45b5d 100755
--- a/tools/testing/selftests/net/netfilter/nft_flowtable.sh
+++ b/tools/testing/selftests/net/netfilter/nft_flowtable.sh
@@ -851,6 +851,210 @@ 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, whose fdb
+# entries are keyed by the pvid of the untagged ports.
+test_bridge_roam()
+{
+       local vid=$1 how=$2 fam=${3:-4} vf=$4
+       local p0=sr0 p1=sr1 r0=rr0 r1=rr1
+       local mac=02:00:00:09:01:99
+       local what="roaming on bridge${vf:+ with vlan_filtering}"
+       what="$what${vid:+ with vlan ports}${how:+ by fdb $how}"
+       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 lpid cpid max=100 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
+
+       ip -net "$rnsr" link set "$r0" master br0
+       ip -net "$rnsr" link set "$r1" master br0
+       ip -net "$rnsr" addr add $hn${dot}1/$len dev br0 $nodad
+       ip -net "$rnsr" link set br0 up
+       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 
br0
+       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
+               max=0 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)
+       roam_stop
+
+       if [ "$how" = age ] &&
+          bridge -n "$rnsr" fdb show br br0 | grep -q "$mac.*master"; 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 [ -z "$slow" ] || [ "$slow" -gt $max ]; 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
+
 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


Reply via email to