xref: /linux/tools/testing/selftests/net/ecmp_rehash.sh (revision 00599d4841790d05401820a96cf7edb193888b00)
1*ae57a553SNeil Spring#!/bin/bash
2*ae57a553SNeil Spring# SPDX-License-Identifier: GPL-2.0
3*ae57a553SNeil Spring#
4*ae57a553SNeil Spring# Test local ECMP path re-selection on TCP retransmission timeout and PLB.
5*ae57a553SNeil Spring#
6*ae57a553SNeil Spring# Two namespaces connected by two parallel veth pairs with a 2-way ECMP
7*ae57a553SNeil Spring# route.  When a TCP path is blocked (via tc drop) or congested (via
8*ae57a553SNeil Spring# netem ECN marking), the kernel rehashes the connection via
9*ae57a553SNeil Spring# sk_rethink_txhash() + __sk_dst_reset(), causing the next route lookup
10*ae57a553SNeil Spring# to select the other ECMP path.
11*ae57a553SNeil Spring#
12*ae57a553SNeil Spring# Expected runtime: ~60 seconds.  Most time is spent waiting for TCP
13*ae57a553SNeil Spring# retransmission timeouts (1-7s per test) and running multi-round
14*ae57a553SNeil Spring# consistency checks (10 rounds each).  The large slowwait/connect-timeout
15*ae57a553SNeil Spring# values (30-120s) are worst-case bounds for CI; a correctly functioning
16*ae57a553SNeil Spring# kernel reaches each check well before the timeout expires.
17*ae57a553SNeil Spring
18*ae57a553SNeil Springsource lib.sh
19*ae57a553SNeil Spring
20*ae57a553SNeil SpringSUBNETS=(a b)
21*ae57a553SNeil SpringPORT=9900
22*ae57a553SNeil Spring: "${ECMP_REBUILD_ROUNDS:=10}"
23*ae57a553SNeil Spring
24*ae57a553SNeil Spring# alloc_ports NAME [COUNT]: set NAME to the next free port and reserve
25*ae57a553SNeil Spring# COUNT ports (default 1) from a shared counter.  Each test allocates its
26*ae57a553SNeil Spring# own port(s) where it runs, so a retry or a newly added test never
27*ae57a553SNeil Spring# collides; the per-round tests reserve ECMP_REBUILD_ROUNDS each.
28*ae57a553SNeil SpringNEXT_PORT=$PORT
29*ae57a553SNeil Springalloc_ports()
30*ae57a553SNeil Spring{
31*ae57a553SNeil Spring	printf -v "$1" '%d' "$NEXT_PORT"
32*ae57a553SNeil Spring	NEXT_PORT=$((NEXT_PORT + ${2:-1}))
33*ae57a553SNeil Spring}
34*ae57a553SNeil Spring
35*ae57a553SNeil SpringALL_TESTS="
36*ae57a553SNeil Spring	test_ecmp_syn_rehash
37*ae57a553SNeil Spring	test_ecmp_synack_rehash
38*ae57a553SNeil Spring	test_ecmp_midstream_rehash
39*ae57a553SNeil Spring	test_ecmp_midstream_ack_rehash
40*ae57a553SNeil Spring	test_ecmp_plb_rehash
41*ae57a553SNeil Spring	test_ecmp_hash_policy1_no_rehash
42*ae57a553SNeil Spring	test_ecmp_no_flowlabel_leak
43*ae57a553SNeil Spring	test_ecmp_dst_rebuild_consistency
44*ae57a553SNeil Spring	test_ecmp_syncookie_path_consistency
45*ae57a553SNeil Spring"
46*ae57a553SNeil Spring
47*ae57a553SNeil Springlink_tx_packets_get()
48*ae57a553SNeil Spring{
49*ae57a553SNeil Spring	local ns=$1; shift
50*ae57a553SNeil Spring	local dev=$1; shift
51*ae57a553SNeil Spring
52*ae57a553SNeil Spring	ip netns exec "$ns" cat "/sys/class/net/$dev/statistics/tx_packets"
53*ae57a553SNeil Spring}
54*ae57a553SNeil Spring
55*ae57a553SNeil Spring# Return the number of packets matched by the tc filter action on a device.
56*ae57a553SNeil Spring# When tc drops packets via "action drop", the device's tx_packets is not
57*ae57a553SNeil Spring# incremented (packet never reaches veth_xmit), but the tc action maintains
58*ae57a553SNeil Spring# its own counter.
59*ae57a553SNeil Springtc_filter_pkt_count()
60*ae57a553SNeil Spring{
61*ae57a553SNeil Spring	local ns=$1; shift
62*ae57a553SNeil Spring	local dev=$1; shift
63*ae57a553SNeil Spring
64*ae57a553SNeil Spring	ip netns exec "$ns" tc -s filter show dev "$dev" parent 1: 2>/dev/null |
65*ae57a553SNeil Spring		awk '/Sent .* pkt/ {
66*ae57a553SNeil Spring			for (i=1; i<=NF; i++)
67*ae57a553SNeil Spring				if ($i == "pkt") { print $(i-1); exit }
68*ae57a553SNeil Spring		}'
69*ae57a553SNeil Spring}
70*ae57a553SNeil Spring
71*ae57a553SNeil Spring# Read a TcpExt counter from /proc/net/netstat in a namespace.
72*ae57a553SNeil Spring# Returns 0 if the counter is not found.
73*ae57a553SNeil Springget_netstat_counter()
74*ae57a553SNeil Spring{
75*ae57a553SNeil Spring	local ns=$1; shift
76*ae57a553SNeil Spring	local field=$1; shift
77*ae57a553SNeil Spring	local val
78*ae57a553SNeil Spring
79*ae57a553SNeil Spring	# shellcheck disable=SC2016
80*ae57a553SNeil Spring	val=$(ip netns exec "$ns" awk -v key="$field" '
81*ae57a553SNeil Spring		/^TcpExt:/ {
82*ae57a553SNeil Spring			if (!h) { split($0, n); h=1 }
83*ae57a553SNeil Spring			else {
84*ae57a553SNeil Spring				split($0, v)
85*ae57a553SNeil Spring				for (i in n)
86*ae57a553SNeil Spring					if (n[i] == key) print v[i]
87*ae57a553SNeil Spring			}
88*ae57a553SNeil Spring		}
89*ae57a553SNeil Spring	' /proc/net/netstat)
90*ae57a553SNeil Spring	echo "${val:-0}"
91*ae57a553SNeil Spring}
92*ae57a553SNeil Spring
93*ae57a553SNeil Spring# Apply netem ECN marking: CE-mark all ECT packets instead of dropping them.
94*ae57a553SNeil Springmark_ecn()
95*ae57a553SNeil Spring{
96*ae57a553SNeil Spring	local ns=$1; shift
97*ae57a553SNeil Spring	local dev=$1; shift
98*ae57a553SNeil Spring
99*ae57a553SNeil Spring	ip netns exec "$ns" tc qdisc add dev "$dev" root netem loss 100% ecn
100*ae57a553SNeil Spring}
101*ae57a553SNeil Spring
102*ae57a553SNeil Spring# Block TCP (IPv6 next-header = 6) egress, allowing ICMPv6 through.
103*ae57a553SNeil Springblock_tcp()
104*ae57a553SNeil Spring{
105*ae57a553SNeil Spring	local ns=$1; shift
106*ae57a553SNeil Spring	local dev=$1; shift
107*ae57a553SNeil Spring
108*ae57a553SNeil Spring	ip netns exec "$ns" tc qdisc add dev "$dev" root handle 1: prio
109*ae57a553SNeil Spring	ip netns exec "$ns" tc filter add dev "$dev" parent 1: \
110*ae57a553SNeil Spring		protocol ipv6 prio 1 u32 match u8 0x06 0xff at 6 action drop
111*ae57a553SNeil Spring}
112*ae57a553SNeil Spring
113*ae57a553SNeil Springunblock_tcp()
114*ae57a553SNeil Spring{
115*ae57a553SNeil Spring	local ns=$1; shift
116*ae57a553SNeil Spring	local dev=$1; shift
117*ae57a553SNeil Spring
118*ae57a553SNeil Spring	ip netns exec "$ns" tc qdisc del dev "$dev" root 2>/dev/null
119*ae57a553SNeil Spring}
120*ae57a553SNeil Spring
121*ae57a553SNeil Spring# Return success when a device's TX counter exceeds a baseline value.
122*ae57a553SNeil Springdev_tx_packets_above()
123*ae57a553SNeil Spring{
124*ae57a553SNeil Spring	local ns=$1; shift
125*ae57a553SNeil Spring	local dev=$1; shift
126*ae57a553SNeil Spring	local baseline=$1; shift
127*ae57a553SNeil Spring
128*ae57a553SNeil Spring	local cur
129*ae57a553SNeil Spring	cur=$(link_tx_packets_get "$ns" "$dev")
130*ae57a553SNeil Spring	[ "$cur" -gt "$baseline" ]
131*ae57a553SNeil Spring}
132*ae57a553SNeil Spring
133*ae57a553SNeil Spring# Return success when both devices have dropped at least one TCP packet.
134*ae57a553SNeil Springboth_devs_attempted()
135*ae57a553SNeil Spring{
136*ae57a553SNeil Spring	local ns=$1; shift
137*ae57a553SNeil Spring	local dev0=$1; shift
138*ae57a553SNeil Spring	local dev1=$1; shift
139*ae57a553SNeil Spring
140*ae57a553SNeil Spring	local c0 c1
141*ae57a553SNeil Spring	c0=$(tc_filter_pkt_count "$ns" "$dev0")
142*ae57a553SNeil Spring	c1=$(tc_filter_pkt_count "$ns" "$dev1")
143*ae57a553SNeil Spring	[ "${c0:-0}" -ge 1 ] && [ "${c1:-0}" -ge 1 ]
144*ae57a553SNeil Spring}
145*ae57a553SNeil Spring
146*ae57a553SNeil Springlink_tx_packets_total()
147*ae57a553SNeil Spring{
148*ae57a553SNeil Spring	local ns=$1; shift
149*ae57a553SNeil Spring	local dev0=${1:-veth0a}; shift 2>/dev/null
150*ae57a553SNeil Spring	local dev1=${1:-veth1a}
151*ae57a553SNeil Spring
152*ae57a553SNeil Spring	echo $(( $(link_tx_packets_get "$ns" "$dev0") +
153*ae57a553SNeil Spring		 $(link_tx_packets_get "$ns" "$dev1") ))
154*ae57a553SNeil Spring}
155*ae57a553SNeil Spring
156*ae57a553SNeil Spring# (Re)install the ECMP multipath routes between NS1 and NS2.  $1 is the
157*ae57a553SNeil Spring# ip route operation ("add" to create, "change" to replace).  If $2 is
158*ae57a553SNeil Spring# given it names a congestion control to pin on both routes via "congctl";
159*ae57a553SNeil Spring# because dctcp carries TCP_CONG_NEEDS_ECN, this also tags the route with
160*ae57a553SNeil Spring# DST_FEATURE_ECN_CA, which makes the server negotiate ECN without the
161*ae57a553SNeil Spring# listener itself having to run dctcp.  The nexthop topology lives here
162*ae57a553SNeil Spring# only, so a test can re-pin the routes and restore them with one call.
163*ae57a553SNeil Springinstall_ecmp_routes()
164*ae57a553SNeil Spring{
165*ae57a553SNeil Spring	local op=$1 cc=$2
166*ae57a553SNeil Spring	local -a cc_attr=()
167*ae57a553SNeil Spring
168*ae57a553SNeil Spring	[ -n "$cc" ] && cc_attr=(congctl "$cc")
169*ae57a553SNeil Spring
170*ae57a553SNeil Spring	ip -n "$NS1" -6 route "$op" fd00:ff::2/128 "${cc_attr[@]}" \
171*ae57a553SNeil Spring		nexthop via fd00:a::2 dev veth0a \
172*ae57a553SNeil Spring		nexthop via fd00:b::2 dev veth1a
173*ae57a553SNeil Spring
174*ae57a553SNeil Spring	ip -n "$NS2" -6 route "$op" fd00:ff::1/128 "${cc_attr[@]}" \
175*ae57a553SNeil Spring		nexthop via fd00:a::1 dev veth0b \
176*ae57a553SNeil Spring		nexthop via fd00:b::1 dev veth1b
177*ae57a553SNeil Spring}
178*ae57a553SNeil Spring
179*ae57a553SNeil Springsetup()
180*ae57a553SNeil Spring{
181*ae57a553SNeil Spring	setup_ns NS1 NS2
182*ae57a553SNeil Spring
183*ae57a553SNeil Spring	local ns
184*ae57a553SNeil Spring	for ns in "$NS1" "$NS2"; do
185*ae57a553SNeil Spring		ip netns exec "$ns" sysctl -qw net.ipv6.conf.all.accept_dad=0
186*ae57a553SNeil Spring		ip netns exec "$ns" sysctl -qw net.ipv6.conf.default.accept_dad=0
187*ae57a553SNeil Spring		ip netns exec "$ns" sysctl -qw net.ipv6.conf.all.forwarding=1
188*ae57a553SNeil Spring		ip netns exec "$ns" sysctl -qw net.core.txrehash=1
189*ae57a553SNeil Spring	done
190*ae57a553SNeil Spring
191*ae57a553SNeil Spring	local i sub
192*ae57a553SNeil Spring	for i in 0 1; do
193*ae57a553SNeil Spring		sub=${SUBNETS[$i]}
194*ae57a553SNeil Spring		ip link add "veth${i}a" type veth peer name "veth${i}b"
195*ae57a553SNeil Spring		ip link set "veth${i}a" netns "$NS1"
196*ae57a553SNeil Spring		ip link set "veth${i}b" netns "$NS2"
197*ae57a553SNeil Spring		ip -n "$NS1" addr add "fd00:${sub}::1/64" dev "veth${i}a"
198*ae57a553SNeil Spring		ip -n "$NS2" addr add "fd00:${sub}::2/64" dev "veth${i}b"
199*ae57a553SNeil Spring		ip -n "$NS1" link set "veth${i}a" up
200*ae57a553SNeil Spring		ip -n "$NS2" link set "veth${i}b" up
201*ae57a553SNeil Spring	done
202*ae57a553SNeil Spring
203*ae57a553SNeil Spring	ip -n "$NS1" addr add fd00:ff::1/128 dev lo
204*ae57a553SNeil Spring	ip -n "$NS2" addr add fd00:ff::2/128 dev lo
205*ae57a553SNeil Spring
206*ae57a553SNeil Spring	# Allow many SYN retries at 1-second intervals (linear, no
207*ae57a553SNeil Spring	# exponential backoff) so the rehash test has enough attempts
208*ae57a553SNeil Spring	# to exercise both ECMP paths.
209*ae57a553SNeil Spring	if ! ip netns exec "$NS1" sysctl -qw \
210*ae57a553SNeil Spring	     net.ipv4.tcp_syn_linear_timeouts=25; then
211*ae57a553SNeil Spring		echo "SKIP: tcp_syn_linear_timeouts not supported"
212*ae57a553SNeil Spring		return "$ksft_skip"
213*ae57a553SNeil Spring	fi
214*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_syn_retries=25
215*ae57a553SNeil Spring
216*ae57a553SNeil Spring	# Keep the server's request socket alive during the blocking
217*ae57a553SNeil Spring	# period so SYN/ACK retransmits continue.
218*ae57a553SNeil Spring	ip netns exec "$NS2" sysctl -qw net.ipv4.tcp_synack_retries=25
219*ae57a553SNeil Spring
220*ae57a553SNeil Spring	install_ecmp_routes add
221*ae57a553SNeil Spring
222*ae57a553SNeil Spring	for i in 0 1; do
223*ae57a553SNeil Spring		sub=${SUBNETS[$i]}
224*ae57a553SNeil Spring		ip netns exec "$NS1" \
225*ae57a553SNeil Spring			ping -6 -c1 -W5 "fd00:${sub}::2" &>/dev/null
226*ae57a553SNeil Spring		ip netns exec "$NS2" \
227*ae57a553SNeil Spring			ping -6 -c1 -W5 "fd00:${sub}::1" &>/dev/null
228*ae57a553SNeil Spring	done
229*ae57a553SNeil Spring
230*ae57a553SNeil Spring	if ! ip netns exec "$NS1" ping -6 -c1 -W5 fd00:ff::2 &>/dev/null; then
231*ae57a553SNeil Spring		echo "Basic connectivity check failed"
232*ae57a553SNeil Spring		return "$ksft_skip"
233*ae57a553SNeil Spring	fi
234*ae57a553SNeil Spring}
235*ae57a553SNeil Spring
236*ae57a553SNeil Spring# Block ALL paths, start a connection, wait until SYNs have been dropped
237*ae57a553SNeil Spring# on both interfaces (proving rehash steered the SYN to a new path), then
238*ae57a553SNeil Spring# unblock so the connection completes.
239*ae57a553SNeil Springtest_ecmp_syn_rehash()
240*ae57a553SNeil Spring{
241*ae57a553SNeil Spring	RET=0
242*ae57a553SNeil Spring	local port
243*ae57a553SNeil Spring	alloc_ports port
244*ae57a553SNeil Spring
245*ae57a553SNeil Spring	block_tcp "$NS1" veth0a
246*ae57a553SNeil Spring	defer unblock_tcp "$NS1" veth0a
247*ae57a553SNeil Spring	block_tcp "$NS1" veth1a
248*ae57a553SNeil Spring	defer unblock_tcp "$NS1" veth1a
249*ae57a553SNeil Spring
250*ae57a553SNeil Spring	ip netns exec "$NS2" socat \
251*ae57a553SNeil Spring		"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr,fork" \
252*ae57a553SNeil Spring		EXEC:"echo ESTABLISH_OK" &
253*ae57a553SNeil Spring	defer kill_process $!
254*ae57a553SNeil Spring
255*ae57a553SNeil Spring	wait_local_port_listen "$NS2" "$port" tcp
256*ae57a553SNeil Spring
257*ae57a553SNeil Spring	local rehash_before
258*ae57a553SNeil Spring	rehash_before=$(get_netstat_counter "$NS1" TcpTimeoutRehash)
259*ae57a553SNeil Spring
260*ae57a553SNeil Spring	# Start the connection in the background; it will retry SYNs at
261*ae57a553SNeil Spring	# 1-second intervals until an unblocked path is found.
262*ae57a553SNeil Spring	# Use -u (unidirectional) to only receive from the server;
263*ae57a553SNeil Spring	# sending data back would risk SIGPIPE if the server's EXEC
264*ae57a553SNeil Spring	# child has already exited.
265*ae57a553SNeil Spring	local tmpfile
266*ae57a553SNeil Spring	tmpfile=$(mktemp)
267*ae57a553SNeil Spring	defer rm -f "$tmpfile"
268*ae57a553SNeil Spring
269*ae57a553SNeil Spring	ip netns exec "$NS1" socat -u \
270*ae57a553SNeil Spring		"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],connect-timeout=60" \
271*ae57a553SNeil Spring		STDOUT >"$tmpfile" 2>&1 &
272*ae57a553SNeil Spring	local client_pid=$!
273*ae57a553SNeil Spring	defer kill_process "$client_pid"
274*ae57a553SNeil Spring
275*ae57a553SNeil Spring	# Wait until both paths have seen at least one dropped SYN.
276*ae57a553SNeil Spring	# This proves sk_rethink_txhash() rehashed the connection from
277*ae57a553SNeil Spring	# one ECMP path to the other.
278*ae57a553SNeil Spring	slowwait 30 both_devs_attempted "$NS1" veth0a veth1a > /dev/null
279*ae57a553SNeil Spring	check_err $? "SYNs did not appear on both paths (rehash not working)"
280*ae57a553SNeil Spring	if [ "$RET" -ne 0 ]; then
281*ae57a553SNeil Spring		log_test "Local ECMP SYN rehash: establish with blocked paths"
282*ae57a553SNeil Spring		return
283*ae57a553SNeil Spring	fi
284*ae57a553SNeil Spring
285*ae57a553SNeil Spring	# Unblock both paths and let the next SYN retransmit succeed.
286*ae57a553SNeil Spring	unblock_tcp "$NS1" veth0a
287*ae57a553SNeil Spring	unblock_tcp "$NS1" veth1a
288*ae57a553SNeil Spring
289*ae57a553SNeil Spring	local rc=0
290*ae57a553SNeil Spring	wait "$client_pid" || rc=$?
291*ae57a553SNeil Spring
292*ae57a553SNeil Spring	local result
293*ae57a553SNeil Spring	result=$(cat "$tmpfile" 2>/dev/null)
294*ae57a553SNeil Spring
295*ae57a553SNeil Spring	if [[ "$result" != *"ESTABLISH_OK"* ]]; then
296*ae57a553SNeil Spring		check_err 1 "connection failed after unblocking (rc=$rc): $result"
297*ae57a553SNeil Spring	fi
298*ae57a553SNeil Spring
299*ae57a553SNeil Spring	local rehash_after
300*ae57a553SNeil Spring	rehash_after=$(get_netstat_counter "$NS1" TcpTimeoutRehash)
301*ae57a553SNeil Spring	if [ "$rehash_after" -le "$rehash_before" ]; then
302*ae57a553SNeil Spring		check_err 1 "TcpTimeoutRehash counter did not increment"
303*ae57a553SNeil Spring	fi
304*ae57a553SNeil Spring
305*ae57a553SNeil Spring	log_test "Local ECMP SYN rehash: establish with blocked paths"
306*ae57a553SNeil Spring}
307*ae57a553SNeil Spring
308*ae57a553SNeil Spring# Block the server's return paths so SYN/ACKs are dropped.  The client
309*ae57a553SNeil Spring# retransmits SYNs at 1-second intervals; each duplicate SYN arriving at
310*ae57a553SNeil Spring# the server triggers tcp_rtx_synack() which re-rolls txhash, so the
311*ae57a553SNeil Spring# retransmitted SYN/ACK selects a different ECMP return path.
312*ae57a553SNeil Springtest_ecmp_synack_rehash()
313*ae57a553SNeil Spring{
314*ae57a553SNeil Spring	RET=0
315*ae57a553SNeil Spring	local port
316*ae57a553SNeil Spring	alloc_ports port
317*ae57a553SNeil Spring
318*ae57a553SNeil Spring	block_tcp "$NS2" veth0b
319*ae57a553SNeil Spring	defer unblock_tcp "$NS2" veth0b
320*ae57a553SNeil Spring	block_tcp "$NS2" veth1b
321*ae57a553SNeil Spring	defer unblock_tcp "$NS2" veth1b
322*ae57a553SNeil Spring
323*ae57a553SNeil Spring	ip netns exec "$NS2" socat \
324*ae57a553SNeil Spring		"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr,fork" \
325*ae57a553SNeil Spring		EXEC:"echo SYNACK_OK" &
326*ae57a553SNeil Spring	defer kill_process $!
327*ae57a553SNeil Spring
328*ae57a553SNeil Spring	wait_local_port_listen "$NS2" "$port" tcp
329*ae57a553SNeil Spring
330*ae57a553SNeil Spring	# Start the connection; SYNs reach the server (client egress is
331*ae57a553SNeil Spring	# open) but SYN/ACKs are dropped on the server's return path.
332*ae57a553SNeil Spring	local tmpfile
333*ae57a553SNeil Spring	tmpfile=$(mktemp)
334*ae57a553SNeil Spring	defer rm -f "$tmpfile"
335*ae57a553SNeil Spring
336*ae57a553SNeil Spring	ip netns exec "$NS1" socat -u \
337*ae57a553SNeil Spring		"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],connect-timeout=60" \
338*ae57a553SNeil Spring		STDOUT >"$tmpfile" 2>&1 &
339*ae57a553SNeil Spring	local client_pid=$!
340*ae57a553SNeil Spring	defer kill_process "$client_pid"
341*ae57a553SNeil Spring
342*ae57a553SNeil Spring	# Wait until both server-side interfaces have dropped at least
343*ae57a553SNeil Spring	# one SYN/ACK, proving the server rehashed its return path.
344*ae57a553SNeil Spring	slowwait 30 both_devs_attempted "$NS2" veth0b veth1b > /dev/null
345*ae57a553SNeil Spring	check_err $? "SYN/ACKs did not appear on both return paths"
346*ae57a553SNeil Spring	if [ "$RET" -ne 0 ]; then
347*ae57a553SNeil Spring		log_test "Local ECMP SYN/ACK rehash: blocked return path"
348*ae57a553SNeil Spring		return
349*ae57a553SNeil Spring	fi
350*ae57a553SNeil Spring
351*ae57a553SNeil Spring	# Unblock and let the connection complete.
352*ae57a553SNeil Spring	unblock_tcp "$NS2" veth0b
353*ae57a553SNeil Spring	unblock_tcp "$NS2" veth1b
354*ae57a553SNeil Spring
355*ae57a553SNeil Spring	local rc=0
356*ae57a553SNeil Spring	wait "$client_pid" || rc=$?
357*ae57a553SNeil Spring
358*ae57a553SNeil Spring	local result
359*ae57a553SNeil Spring	result=$(cat "$tmpfile" 2>/dev/null)
360*ae57a553SNeil Spring
361*ae57a553SNeil Spring	if [[ "$result" != *"SYNACK_OK"* ]]; then
362*ae57a553SNeil Spring		check_err 1 "connection failed after unblocking (rc=$rc): $result"
363*ae57a553SNeil Spring	fi
364*ae57a553SNeil Spring
365*ae57a553SNeil Spring	log_test "Local ECMP SYN/ACK rehash: blocked return path"
366*ae57a553SNeil Spring}
367*ae57a553SNeil Spring
368*ae57a553SNeil Spring# Establish a data transfer with both paths open, then block the
369*ae57a553SNeil Spring# active path.  Verify that data appears on the previously inactive
370*ae57a553SNeil Spring# path (proving RTO triggered a rehash) and that TcpTimeoutRehash
371*ae57a553SNeil Spring# incremented.
372*ae57a553SNeil Spring#
373*ae57a553SNeil Spring# With 2-way ECMP each rehash may pick the same path, so a single
374*ae57a553SNeil Spring# attempt can occasionally fail.  Retry once for robustness.
375*ae57a553SNeil Spring
376*ae57a553SNeil Spring# Single attempt at the midstream rehash check.  Returns 0 on success.
377*ae57a553SNeil Springecmp_midstream_rehash_attempt()
378*ae57a553SNeil Spring{
379*ae57a553SNeil Spring	local port=$1; shift
380*ae57a553SNeil Spring	local reason=""
381*ae57a553SNeil Spring
382*ae57a553SNeil Spring	ip netns exec "$NS2" socat -u \
383*ae57a553SNeil Spring		"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" - >/dev/null &
384*ae57a553SNeil Spring	local server_pid=$!
385*ae57a553SNeil Spring
386*ae57a553SNeil Spring	wait_local_port_listen "$NS2" "$port" tcp
387*ae57a553SNeil Spring
388*ae57a553SNeil Spring	local base_tx0 base_tx1
389*ae57a553SNeil Spring	base_tx0=$(link_tx_packets_get "$NS1" veth0a)
390*ae57a553SNeil Spring	base_tx1=$(link_tx_packets_get "$NS1" veth1a)
391*ae57a553SNeil Spring
392*ae57a553SNeil Spring	# Continuous data source; timeout caps overall test duration and
393*ae57a553SNeil Spring	# must exceed the slowwait below so data keeps flowing.
394*ae57a553SNeil Spring	ip netns exec "$NS1" timeout 90 socat -u \
395*ae57a553SNeil Spring		OPEN:/dev/zero \
396*ae57a553SNeil Spring		"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1]" &>/dev/null &
397*ae57a553SNeil Spring	local client_pid=$!
398*ae57a553SNeil Spring
399*ae57a553SNeil Spring	# Wait for enough packets to identify the active path.
400*ae57a553SNeil Spring	if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \
401*ae57a553SNeil Spring			">= $((base_tx0 + base_tx1 + 10))" \
402*ae57a553SNeil Spring		link_tx_packets_total "$NS1" > /dev/null; then
403*ae57a553SNeil Spring		kill "$client_pid" "$server_pid" 2>/dev/null
404*ae57a553SNeil Spring		wait "$client_pid" "$server_pid" 2>/dev/null
405*ae57a553SNeil Spring		echo "no TX activity"
406*ae57a553SNeil Spring		return 1
407*ae57a553SNeil Spring	fi
408*ae57a553SNeil Spring
409*ae57a553SNeil Spring	# Find the active path and block it.
410*ae57a553SNeil Spring	local current_tx0 current_tx1 active_idx inactive_idx
411*ae57a553SNeil Spring	current_tx0=$(link_tx_packets_get "$NS1" veth0a)
412*ae57a553SNeil Spring	current_tx1=$(link_tx_packets_get "$NS1" veth1a)
413*ae57a553SNeil Spring	if [ $((current_tx0 - base_tx0)) -ge $((current_tx1 - base_tx1)) ]; then
414*ae57a553SNeil Spring		active_idx=0; inactive_idx=1
415*ae57a553SNeil Spring	else
416*ae57a553SNeil Spring		active_idx=1; inactive_idx=0
417*ae57a553SNeil Spring	fi
418*ae57a553SNeil Spring
419*ae57a553SNeil Spring	local rehash_before
420*ae57a553SNeil Spring	rehash_before=$(get_netstat_counter "$NS1" TcpTimeoutRehash)
421*ae57a553SNeil Spring	# Suppress __dst_negative_advice() in tcp_write_timeout() so
422*ae57a553SNeil Spring	# that __sk_dst_reset() is the only dst-invalidation mechanism
423*ae57a553SNeil Spring	# on the RTO path.
424*ae57a553SNeil Spring	local saved_retries1
425*ae57a553SNeil Spring	saved_retries1=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_retries1)
426*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_retries1=255
427*ae57a553SNeil Spring
428*ae57a553SNeil Spring	block_tcp "$NS1" "veth${active_idx}a"
429*ae57a553SNeil Spring
430*ae57a553SNeil Spring	# Capture baseline after block_tcp returns.  block_tcp adds a
431*ae57a553SNeil Spring	# prio qdisc then a tc filter; between those two steps the
432*ae57a553SNeil Spring	# qdisc's CAN_BYPASS fast-path lets packets through unfiltered.
433*ae57a553SNeil Spring	local inactive_before
434*ae57a553SNeil Spring	inactive_before=$(link_tx_packets_get "$NS1" "veth${inactive_idx}a")
435*ae57a553SNeil Spring
436*ae57a553SNeil Spring	# Wait for meaningful data on the previously inactive path,
437*ae57a553SNeil Spring	# proving RTO triggered a rehash and data actually moved.
438*ae57a553SNeil Spring	if ! slowwait 60 dev_tx_packets_above \
439*ae57a553SNeil Spring		"$NS1" "veth${inactive_idx}a" "$((inactive_before + 100))" \
440*ae57a553SNeil Spring		> /dev/null; then
441*ae57a553SNeil Spring		reason="no data on alternate path"
442*ae57a553SNeil Spring	fi
443*ae57a553SNeil Spring
444*ae57a553SNeil Spring	local rehash_after
445*ae57a553SNeil Spring	rehash_after=$(get_netstat_counter "$NS1" TcpTimeoutRehash)
446*ae57a553SNeil Spring	if [ "$rehash_after" -le "$rehash_before" ]; then
447*ae57a553SNeil Spring		reason="${reason:+$reason; }TcpTimeoutRehash did not increment"
448*ae57a553SNeil Spring	fi
449*ae57a553SNeil Spring
450*ae57a553SNeil Spring	unblock_tcp "$NS1" "veth${active_idx}a"
451*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw \
452*ae57a553SNeil Spring		net.ipv4.tcp_retries1="$saved_retries1"
453*ae57a553SNeil Spring	kill "$client_pid" "$server_pid" 2>/dev/null
454*ae57a553SNeil Spring	wait "$client_pid" "$server_pid" 2>/dev/null
455*ae57a553SNeil Spring	if [ -n "$reason" ]; then
456*ae57a553SNeil Spring		echo "$reason"
457*ae57a553SNeil Spring		return 1
458*ae57a553SNeil Spring	fi
459*ae57a553SNeil Spring	return 0
460*ae57a553SNeil Spring}
461*ae57a553SNeil Spring
462*ae57a553SNeil Springtest_ecmp_midstream_rehash()
463*ae57a553SNeil Spring{
464*ae57a553SNeil Spring	RET=0
465*ae57a553SNeil Spring	local port retry_port
466*ae57a553SNeil Spring	alloc_ports port
467*ae57a553SNeil Spring	alloc_ports retry_port
468*ae57a553SNeil Spring
469*ae57a553SNeil Spring	local fail_reason
470*ae57a553SNeil Spring	if ! ecmp_midstream_rehash_attempt "$port" >/dev/null; then
471*ae57a553SNeil Spring		fail_reason=$(ecmp_midstream_rehash_attempt "$retry_port")
472*ae57a553SNeil Spring		check_err $? "$fail_reason"
473*ae57a553SNeil Spring	fi
474*ae57a553SNeil Spring
475*ae57a553SNeil Spring	log_test "Local ECMP midstream rehash: block active path"
476*ae57a553SNeil Spring}
477*ae57a553SNeil Spring
478*ae57a553SNeil Spring# Single attempt at the ACK rehash check.  Returns 0 on success.
479*ae57a553SNeil Springecmp_ack_rehash_attempt()
480*ae57a553SNeil Spring{
481*ae57a553SNeil Spring	local port=$1; shift
482*ae57a553SNeil Spring	local reason=""
483*ae57a553SNeil Spring
484*ae57a553SNeil Spring	ip netns exec "$NS2" socat -u \
485*ae57a553SNeil Spring		"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" - >/dev/null &
486*ae57a553SNeil Spring	local server_pid=$!
487*ae57a553SNeil Spring
488*ae57a553SNeil Spring	wait_local_port_listen "$NS2" "$port" tcp
489*ae57a553SNeil Spring
490*ae57a553SNeil Spring	local base_tx0 base_tx1
491*ae57a553SNeil Spring	base_tx0=$(link_tx_packets_get "$NS2" veth0b)
492*ae57a553SNeil Spring	base_tx1=$(link_tx_packets_get "$NS2" veth1b)
493*ae57a553SNeil Spring
494*ae57a553SNeil Spring	# Continuous data source from NS1 to NS2.  Cap the send buffer
495*ae57a553SNeil Spring	# so in-flight data stays below the receiver's advertised window.
496*ae57a553SNeil Spring	# Without this, the sender can exhaust the receiver's window and
497*ae57a553SNeil Spring	# enter persist mode (zero-window probing) instead of RTO when
498*ae57a553SNeil Spring	# ACKs are blocked, and persist probes do not trigger flowlabel
499*ae57a553SNeil Spring	# rehash.
500*ae57a553SNeil Spring	ip netns exec "$NS1" timeout 120 socat -u \
501*ae57a553SNeil Spring		OPEN:/dev/zero \
502*ae57a553SNeil Spring		"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],sndbuf=16384" \
503*ae57a553SNeil Spring		&>/dev/null &
504*ae57a553SNeil Spring	local client_pid=$!
505*ae57a553SNeil Spring
506*ae57a553SNeil Spring	# Wait for enough server TX (ACKs) to identify the active return path.
507*ae57a553SNeil Spring	if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \
508*ae57a553SNeil Spring			">= $((base_tx0 + base_tx1 + 10))" \
509*ae57a553SNeil Spring		link_tx_packets_total "$NS2" veth0b veth1b > /dev/null; then
510*ae57a553SNeil Spring		kill "$client_pid" "$server_pid" 2>/dev/null
511*ae57a553SNeil Spring		wait "$client_pid" "$server_pid" 2>/dev/null
512*ae57a553SNeil Spring		echo "no server TX activity"
513*ae57a553SNeil Spring		return 1
514*ae57a553SNeil Spring	fi
515*ae57a553SNeil Spring
516*ae57a553SNeil Spring	local cur_tx0 cur_tx1 active_dev inactive_dev
517*ae57a553SNeil Spring	cur_tx0=$(link_tx_packets_get "$NS2" veth0b)
518*ae57a553SNeil Spring	cur_tx1=$(link_tx_packets_get "$NS2" veth1b)
519*ae57a553SNeil Spring	if [ $((cur_tx0 - base_tx0)) -ge $((cur_tx1 - base_tx1)) ]; then
520*ae57a553SNeil Spring		active_dev=veth0b; inactive_dev=veth1b
521*ae57a553SNeil Spring	else
522*ae57a553SNeil Spring		active_dev=veth1b; inactive_dev=veth0b
523*ae57a553SNeil Spring	fi
524*ae57a553SNeil Spring
525*ae57a553SNeil Spring	local rehash_before
526*ae57a553SNeil Spring	rehash_before=$(get_netstat_counter "$NS2" TcpDuplicateDataRehash)
527*ae57a553SNeil Spring
528*ae57a553SNeil Spring	# Block the inactive return path first (no effect on current
529*ae57a553SNeil Spring	# ACK flow), then block the active path.  This avoids counting
530*ae57a553SNeil Spring	# normal ACK drops as rehash evidence.
531*ae57a553SNeil Spring	block_tcp "$NS2" "$inactive_dev"
532*ae57a553SNeil Spring	local inactive_before
533*ae57a553SNeil Spring	inactive_before=$(tc_filter_pkt_count "$NS2" "$inactive_dev")
534*ae57a553SNeil Spring	block_tcp "$NS2" "$active_dev"
535*ae57a553SNeil Spring
536*ae57a553SNeil Spring	# NS1 will RTO (no ACKs), retransmit with new flowlabel.
537*ae57a553SNeil Spring	# NS2 detects the flowlabel change via tcp_rcv_spurious_retrans(),
538*ae57a553SNeil Spring	# rehashes, and NS2's ACKs try the previously inactive return
539*ae57a553SNeil Spring	# path.  One successful rehash is sufficient.
540*ae57a553SNeil Spring	if ! slowwait 60 until_counter_is \
541*ae57a553SNeil Spring			">= $((${inactive_before:-0} + 1))" \
542*ae57a553SNeil Spring		tc_filter_pkt_count "$NS2" "$inactive_dev" > /dev/null; then
543*ae57a553SNeil Spring		reason="no ACKs on alternate return path after blocking"
544*ae57a553SNeil Spring	fi
545*ae57a553SNeil Spring
546*ae57a553SNeil Spring	local rehash_after
547*ae57a553SNeil Spring	rehash_after=$(get_netstat_counter "$NS2" TcpDuplicateDataRehash)
548*ae57a553SNeil Spring	if [ "$rehash_after" -le "$rehash_before" ]; then
549*ae57a553SNeil Spring		reason="${reason:+$reason; }TcpDuplicateDataRehash did not increment"
550*ae57a553SNeil Spring	fi
551*ae57a553SNeil Spring
552*ae57a553SNeil Spring	unblock_tcp "$NS2" "$active_dev"
553*ae57a553SNeil Spring	unblock_tcp "$NS2" "$inactive_dev"
554*ae57a553SNeil Spring	kill "$client_pid" "$server_pid" 2>/dev/null
555*ae57a553SNeil Spring	wait "$client_pid" "$server_pid" 2>/dev/null
556*ae57a553SNeil Spring	if [ -n "$reason" ]; then
557*ae57a553SNeil Spring		echo "$reason"
558*ae57a553SNeil Spring		return 1
559*ae57a553SNeil Spring	fi
560*ae57a553SNeil Spring	return 0
561*ae57a553SNeil Spring}
562*ae57a553SNeil Spring
563*ae57a553SNeil Spring# Block the receiver's (NS2) ACK return paths while data flows from
564*ae57a553SNeil Spring# NS1 to NS2.  The sender (NS1) times out and retransmits with a new
565*ae57a553SNeil Spring# flowlabel; the receiver detects the changed flowlabel via
566*ae57a553SNeil Spring# tcp_rcv_spurious_retrans() and rehashes its own txhash so that its
567*ae57a553SNeil Spring# ACKs try a different ECMP return path.
568*ae57a553SNeil Spring#
569*ae57a553SNeil Spring# With 2-way ECMP each rehash may pick the same path, so a single
570*ae57a553SNeil Spring# attempt can occasionally fail.  Retry once for robustness.
571*ae57a553SNeil Springtest_ecmp_midstream_ack_rehash()
572*ae57a553SNeil Spring{
573*ae57a553SNeil Spring	RET=0
574*ae57a553SNeil Spring	local port retry_port
575*ae57a553SNeil Spring	alloc_ports port
576*ae57a553SNeil Spring	alloc_ports retry_port
577*ae57a553SNeil Spring
578*ae57a553SNeil Spring	local fail_reason
579*ae57a553SNeil Spring	if ! ecmp_ack_rehash_attempt "$port" >/dev/null; then
580*ae57a553SNeil Spring		fail_reason=$(ecmp_ack_rehash_attempt "$retry_port")
581*ae57a553SNeil Spring		check_err $? "$fail_reason"
582*ae57a553SNeil Spring	fi
583*ae57a553SNeil Spring
584*ae57a553SNeil Spring	log_test "Local ECMP midstream ACK rehash: blocked return path"
585*ae57a553SNeil Spring}
586*ae57a553SNeil Spring
587*ae57a553SNeil Spring# Establish a DCTCP data transfer with PLB enabled, then ECN-mark both
588*ae57a553SNeil Spring# paths.  Sustained CE marking triggers PLB to call sk_rethink_txhash()
589*ae57a553SNeil Spring# + __sk_dst_reset(), bouncing the connection between ECMP paths.
590*ae57a553SNeil Spring# Verify data appears on both paths and that TCPPLBRehash incremented.
591*ae57a553SNeil Springtest_ecmp_plb_rehash()
592*ae57a553SNeil Spring{
593*ae57a553SNeil Spring	RET=0
594*ae57a553SNeil Spring	local port
595*ae57a553SNeil Spring	alloc_ports port
596*ae57a553SNeil Spring
597*ae57a553SNeil Spring	# PLB needs DCTCP, a restricted congestion control.  Adding it to
598*ae57a553SNeil Spring	# the host-global tcp_allowed_congestion_control would relax the
599*ae57a553SNeil Spring	# restricted-CC policy for the whole host (there is no per-netns
600*ae57a553SNeil Spring	# allowed set).  Instead pin dctcp on the test routes with
601*ae57a553SNeil Spring	# "congctl": the route's RTAX_CC_ALGO is honoured on both the
602*ae57a553SNeil Spring	# connect and accept paths without the restricted-CC check, and a
603*ae57a553SNeil Spring	# dctcp route also carries DST_FEATURE_ECN_CA so the server
604*ae57a553SNeil Spring	# negotiates ECN -- all confined to the test namespaces.
605*ae57a553SNeil Spring	local available
606*ae57a553SNeil Spring	available=$(ip netns exec "$NS1" sysctl -n \
607*ae57a553SNeil Spring		net.ipv4.tcp_available_congestion_control)
608*ae57a553SNeil Spring	if ! echo "$available" | grep -qw dctcp; then
609*ae57a553SNeil Spring		log_test_skip "Local ECMP PLB rehash: DCTCP not available"
610*ae57a553SNeil Spring		return "$ksft_skip"
611*ae57a553SNeil Spring	fi
612*ae57a553SNeil Spring	install_ecmp_routes change dctcp
613*ae57a553SNeil Spring	defer install_ecmp_routes change
614*ae57a553SNeil Spring
615*ae57a553SNeil Spring	# Save NS1 sysctls before modifying them.
616*ae57a553SNeil Spring	local saved_ecn1 saved_plb_enabled saved_plb_rounds
617*ae57a553SNeil Spring	local saved_plb_thresh saved_plb_suspend
618*ae57a553SNeil Spring	saved_ecn1=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_ecn)
619*ae57a553SNeil Spring	saved_plb_enabled=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_plb_enabled)
620*ae57a553SNeil Spring	saved_plb_rounds=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_plb_rehash_rounds)
621*ae57a553SNeil Spring	saved_plb_thresh=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_plb_cong_thresh)
622*ae57a553SNeil Spring	saved_plb_suspend=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_plb_suspend_rto_sec)
623*ae57a553SNeil Spring
624*ae57a553SNeil Spring	# Enable ECN and PLB on the sender; dctcp comes from the route.
625*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_ecn=1
626*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_enabled=1
627*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_rehash_rounds=3
628*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_cong_thresh=1
629*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_suspend_rto_sec=0
630*ae57a553SNeil Spring	defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_ecn="$saved_ecn1"
631*ae57a553SNeil Spring	defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_enabled="$saved_plb_enabled"
632*ae57a553SNeil Spring	defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_rehash_rounds="$saved_plb_rounds"
633*ae57a553SNeil Spring	defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_cong_thresh="$saved_plb_thresh"
634*ae57a553SNeil Spring	defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_suspend_rto_sec="$saved_plb_suspend"
635*ae57a553SNeil Spring
636*ae57a553SNeil Spring	ip netns exec "$NS2" socat -u \
637*ae57a553SNeil Spring		"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" - >/dev/null &
638*ae57a553SNeil Spring	defer kill_process $!
639*ae57a553SNeil Spring
640*ae57a553SNeil Spring	wait_local_port_listen "$NS2" "$port" tcp
641*ae57a553SNeil Spring
642*ae57a553SNeil Spring	local base_tx0 base_tx1
643*ae57a553SNeil Spring	base_tx0=$(link_tx_packets_get "$NS1" veth0a)
644*ae57a553SNeil Spring	base_tx1=$(link_tx_packets_get "$NS1" veth1a)
645*ae57a553SNeil Spring
646*ae57a553SNeil Spring	ip netns exec "$NS1" timeout 90 socat -u \
647*ae57a553SNeil Spring		OPEN:/dev/zero \
648*ae57a553SNeil Spring		"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1]" &>/dev/null &
649*ae57a553SNeil Spring	local client_pid=$!
650*ae57a553SNeil Spring	defer kill_process "$client_pid"
651*ae57a553SNeil Spring
652*ae57a553SNeil Spring	# Wait for data to start flowing before applying ECN marking.
653*ae57a553SNeil Spring	busywait "$BUSYWAIT_TIMEOUT" until_counter_is \
654*ae57a553SNeil Spring			">= $((base_tx0 + base_tx1 + 10))" \
655*ae57a553SNeil Spring		link_tx_packets_total "$NS1" > /dev/null
656*ae57a553SNeil Spring	check_err $? "no TX activity detected"
657*ae57a553SNeil Spring	if [ "$RET" -ne 0 ]; then
658*ae57a553SNeil Spring		log_test "Local ECMP PLB rehash: ECN-marked path"
659*ae57a553SNeil Spring		return
660*ae57a553SNeil Spring	fi
661*ae57a553SNeil Spring
662*ae57a553SNeil Spring	# Snapshot TX counters and rehash stats before ECN marking.
663*ae57a553SNeil Spring	local pre_ecn_tx0 pre_ecn_tx1
664*ae57a553SNeil Spring	pre_ecn_tx0=$(link_tx_packets_get "$NS1" veth0a)
665*ae57a553SNeil Spring	pre_ecn_tx1=$(link_tx_packets_get "$NS1" veth1a)
666*ae57a553SNeil Spring
667*ae57a553SNeil Spring	local plb_before rto_before
668*ae57a553SNeil Spring	plb_before=$(get_netstat_counter "$NS1" TCPPLBRehash)
669*ae57a553SNeil Spring	rto_before=$(get_netstat_counter "$NS1" TcpTimeoutRehash)
670*ae57a553SNeil Spring
671*ae57a553SNeil Spring	# CE-mark all data on both paths.  PLB detects sustained
672*ae57a553SNeil Spring	# congestion and rehashes, bouncing traffic between paths.
673*ae57a553SNeil Spring	mark_ecn "$NS1" veth0a
674*ae57a553SNeil Spring	defer unblock_tcp "$NS1" veth0a	# removes the marking rule
675*ae57a553SNeil Spring	mark_ecn "$NS1" veth1a
676*ae57a553SNeil Spring	defer unblock_tcp "$NS1" veth1a	# removes the marking rule
677*ae57a553SNeil Spring
678*ae57a553SNeil Spring	# Wait for meaningful data on both paths, proving PLB rehashed
679*ae57a553SNeil Spring	# the connection and traffic actually moved.  Require at least
680*ae57a553SNeil Spring	# 100 packets beyond the baseline to rule out stray control
681*ae57a553SNeil Spring	# packets (ND, etc.) satisfying the check.
682*ae57a553SNeil Spring	slowwait 60 dev_tx_packets_above \
683*ae57a553SNeil Spring		"$NS1" veth0a "$((pre_ecn_tx0 + 100))" > /dev/null
684*ae57a553SNeil Spring	check_err $? "no data on veth0a after ECN marking"
685*ae57a553SNeil Spring
686*ae57a553SNeil Spring	slowwait 60 dev_tx_packets_above \
687*ae57a553SNeil Spring		"$NS1" veth1a "$((pre_ecn_tx1 + 100))" > /dev/null
688*ae57a553SNeil Spring	check_err $? "no data on veth1a after ECN marking"
689*ae57a553SNeil Spring
690*ae57a553SNeil Spring	local plb_after rto_after
691*ae57a553SNeil Spring	plb_after=$(get_netstat_counter "$NS1" TCPPLBRehash)
692*ae57a553SNeil Spring	rto_after=$(get_netstat_counter "$NS1" TcpTimeoutRehash)
693*ae57a553SNeil Spring	if [ "$plb_after" -le "$plb_before" ]; then
694*ae57a553SNeil Spring		check_err 1 "TCPPLBRehash counter did not increment"
695*ae57a553SNeil Spring	fi
696*ae57a553SNeil Spring	if [ "$rto_after" -gt "$rto_before" ]; then
697*ae57a553SNeil Spring		check_err 1 "TcpTimeoutRehash incremented; rehash was RTO-driven, not PLB"
698*ae57a553SNeil Spring	fi
699*ae57a553SNeil Spring
700*ae57a553SNeil Spring	log_test "Local ECMP PLB rehash: ECN-marked path"
701*ae57a553SNeil Spring}
702*ae57a553SNeil Spring
703*ae57a553SNeil Spring# Verify that hash policy 1 (L3+L4 symmetric) preserves the ECMP path
704*ae57a553SNeil Spring# across rehash.  Policy 1 computes a deterministic hash from the
705*ae57a553SNeil Spring# 5-tuple, so mp_hash stays 0 and rt6_multipath_hash() always selects
706*ae57a553SNeil Spring# the same path regardless of txhash changes.
707*ae57a553SNeil Springtest_ecmp_hash_policy1_no_rehash()
708*ae57a553SNeil Spring{
709*ae57a553SNeil Spring	RET=0
710*ae57a553SNeil Spring	local port
711*ae57a553SNeil Spring	alloc_ports port
712*ae57a553SNeil Spring
713*ae57a553SNeil Spring	local saved_policy
714*ae57a553SNeil Spring	saved_policy=$(ip netns exec "$NS1" sysctl -n \
715*ae57a553SNeil Spring		net.ipv6.fib_multipath_hash_policy)
716*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv6.fib_multipath_hash_policy=1
717*ae57a553SNeil Spring	defer ip netns exec "$NS1" sysctl -qw \
718*ae57a553SNeil Spring		net.ipv6.fib_multipath_hash_policy="$saved_policy"
719*ae57a553SNeil Spring
720*ae57a553SNeil Spring	block_tcp "$NS1" veth0a
721*ae57a553SNeil Spring	defer unblock_tcp "$NS1" veth0a
722*ae57a553SNeil Spring	block_tcp "$NS1" veth1a
723*ae57a553SNeil Spring	defer unblock_tcp "$NS1" veth1a
724*ae57a553SNeil Spring
725*ae57a553SNeil Spring	ip netns exec "$NS2" socat \
726*ae57a553SNeil Spring		"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr,fork" \
727*ae57a553SNeil Spring		EXEC:"echo POLICY1_OK" &
728*ae57a553SNeil Spring	defer kill_process $!
729*ae57a553SNeil Spring
730*ae57a553SNeil Spring	wait_local_port_listen "$NS2" "$port" tcp
731*ae57a553SNeil Spring
732*ae57a553SNeil Spring	local rehash_before
733*ae57a553SNeil Spring	rehash_before=$(get_netstat_counter "$NS1" TcpTimeoutRehash)
734*ae57a553SNeil Spring
735*ae57a553SNeil Spring	ip netns exec "$NS1" timeout 10 socat -u \
736*ae57a553SNeil Spring		"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],connect-timeout=8" \
737*ae57a553SNeil Spring		STDOUT >/dev/null 2>&1 &
738*ae57a553SNeil Spring	local client_pid=$!
739*ae57a553SNeil Spring	defer kill_process "$client_pid"
740*ae57a553SNeil Spring
741*ae57a553SNeil Spring	# With policy 1, the deterministic 5-tuple hash always selects
742*ae57a553SNeil Spring	# the same path.  Wait for multiple SYN retransmits (proving
743*ae57a553SNeil Spring	# rehash was attempted), then verify all SYNs landed on the
744*ae57a553SNeil Spring	# same interface.
745*ae57a553SNeil Spring	local rehash_after
746*ae57a553SNeil Spring	slowwait 8 until_counter_is ">= $((rehash_before + 3))" \
747*ae57a553SNeil Spring		get_netstat_counter "$NS1" TcpTimeoutRehash > /dev/null
748*ae57a553SNeil Spring	rehash_after=$(get_netstat_counter "$NS1" TcpTimeoutRehash)
749*ae57a553SNeil Spring	if [ "$rehash_after" -le "$rehash_before" ]; then
750*ae57a553SNeil Spring		check_err 1 "TcpTimeoutRehash counter did not increment"
751*ae57a553SNeil Spring	fi
752*ae57a553SNeil Spring
753*ae57a553SNeil Spring	local c0 c1
754*ae57a553SNeil Spring	c0=$(tc_filter_pkt_count "$NS1" veth0a)
755*ae57a553SNeil Spring	c1=$(tc_filter_pkt_count "$NS1" veth1a)
756*ae57a553SNeil Spring	if [ "${c0:-0}" -ge 1 ] && [ "${c1:-0}" -ge 1 ]; then
757*ae57a553SNeil Spring		check_err 1 "SYNs appeared on both paths despite policy 1"
758*ae57a553SNeil Spring	fi
759*ae57a553SNeil Spring	if [ "${c0:-0}" -eq 0 ] && [ "${c1:-0}" -eq 0 ]; then
760*ae57a553SNeil Spring		check_err 1 "no SYNs observed on either path"
761*ae57a553SNeil Spring	fi
762*ae57a553SNeil Spring
763*ae57a553SNeil Spring	log_test "Local ECMP policy 1: no path change on rehash"
764*ae57a553SNeil Spring}
765*ae57a553SNeil Spring
766*ae57a553SNeil Spring# Verify that mp_hash does not leak into the on-wire flowlabel.
767*ae57a553SNeil Spring# With auto_flowlabels=0, the wire flowlabel must be 0.  Install tc
768*ae57a553SNeil Spring# filters that pass TCP with flowlabel=0 but drop TCP with nonzero
769*ae57a553SNeil Spring# flowlabel, then establish a connection and transfer data.  If
770*ae57a553SNeil Spring# mp_hash leaked into fl6->flowlabel, the SYN or data packets would
771*ae57a553SNeil Spring# be dropped and the connection would fail.
772*ae57a553SNeil Springtest_ecmp_no_flowlabel_leak()
773*ae57a553SNeil Spring{
774*ae57a553SNeil Spring	RET=0
775*ae57a553SNeil Spring	local port
776*ae57a553SNeil Spring	alloc_ports port
777*ae57a553SNeil Spring
778*ae57a553SNeil Spring	local saved_afl
779*ae57a553SNeil Spring	saved_afl=$(ip netns exec "$NS1" sysctl -n \
780*ae57a553SNeil Spring		net.ipv6.auto_flowlabels)
781*ae57a553SNeil Spring	ip netns exec "$NS1" sysctl -qw net.ipv6.auto_flowlabels=0
782*ae57a553SNeil Spring	defer ip netns exec "$NS1" sysctl -qw \
783*ae57a553SNeil Spring		net.ipv6.auto_flowlabels="$saved_afl"
784*ae57a553SNeil Spring
785*ae57a553SNeil Spring	# On both egress interfaces: pass TCP with flowlabel=0 (prio 1),
786*ae57a553SNeil Spring	# drop any remaining TCP (nonzero flowlabel, prio 2).  ICMPv6
787*ae57a553SNeil Spring	# matches neither filter and passes through normally.
788*ae57a553SNeil Spring	local dev
789*ae57a553SNeil Spring	for dev in veth0a veth1a; do
790*ae57a553SNeil Spring		ip netns exec "$NS1" tc qdisc add dev "$dev" \
791*ae57a553SNeil Spring			root handle 1: prio
792*ae57a553SNeil Spring		ip netns exec "$NS1" tc filter add dev "$dev" parent 1: \
793*ae57a553SNeil Spring			protocol ipv6 prio 1 u32 \
794*ae57a553SNeil Spring			match u32 0x00000000 0x000FFFFF at 0 \
795*ae57a553SNeil Spring			match u8 0x06 0xff at 6 \
796*ae57a553SNeil Spring			action ok
797*ae57a553SNeil Spring		ip netns exec "$NS1" tc filter add dev "$dev" parent 1: \
798*ae57a553SNeil Spring			protocol ipv6 prio 2 u32 \
799*ae57a553SNeil Spring			match u8 0x06 0xff at 6 \
800*ae57a553SNeil Spring			action drop
801*ae57a553SNeil Spring		defer unblock_tcp "$NS1" "$dev"
802*ae57a553SNeil Spring	done
803*ae57a553SNeil Spring
804*ae57a553SNeil Spring	ip netns exec "$NS2" socat \
805*ae57a553SNeil Spring		"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" \
806*ae57a553SNeil Spring		EXEC:"echo FLOWLABEL_OK" &
807*ae57a553SNeil Spring	defer kill_process $!
808*ae57a553SNeil Spring
809*ae57a553SNeil Spring	wait_local_port_listen "$NS2" "$port" tcp
810*ae57a553SNeil Spring
811*ae57a553SNeil Spring	local tmpfile
812*ae57a553SNeil Spring	tmpfile=$(mktemp)
813*ae57a553SNeil Spring	defer rm -f "$tmpfile"
814*ae57a553SNeil Spring
815*ae57a553SNeil Spring	ip netns exec "$NS1" socat -u \
816*ae57a553SNeil Spring		"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],connect-timeout=10" \
817*ae57a553SNeil Spring		STDOUT >"$tmpfile" 2>&1
818*ae57a553SNeil Spring
819*ae57a553SNeil Spring	local result
820*ae57a553SNeil Spring	result=$(cat "$tmpfile" 2>/dev/null)
821*ae57a553SNeil Spring	if [[ "$result" != *"FLOWLABEL_OK"* ]]; then
822*ae57a553SNeil Spring		check_err 1 "connection failed: mp_hash may have leaked into wire flowlabel"
823*ae57a553SNeil Spring	fi
824*ae57a553SNeil Spring
825*ae57a553SNeil Spring	log_test "No flowlabel leak with auto_flowlabels=0"
826*ae57a553SNeil Spring}
827*ae57a553SNeil Spring
828*ae57a553SNeil Spring# Helper: stream data, invalidate the cached dst by adding and
829*ae57a553SNeil Spring# removing a dummy route (bumps fib6_node sernum), then check that
830*ae57a553SNeil Spring# traffic stays on the same ECMP path.  Used by both the normal
831*ae57a553SNeil Spring# tcp_v6_connect and syncookie variants.
832*ae57a553SNeil Springecmp_dst_rebuild_check()
833*ae57a553SNeil Spring{
834*ae57a553SNeil Spring	local ns_client=$1; shift
835*ae57a553SNeil Spring	local port=$1; shift
836*ae57a553SNeil Spring	local rc=0
837*ae57a553SNeil Spring
838*ae57a553SNeil Spring	# Suppress __dst_negative_advice() during the test so that a
839*ae57a553SNeil Spring	# real TCP timeout cannot trigger an additional dst
840*ae57a553SNeil Spring	# invalidation via a different code path.
841*ae57a553SNeil Spring	local saved_retries1
842*ae57a553SNeil Spring	saved_retries1=$(ip netns exec "$ns_client" sysctl -n \
843*ae57a553SNeil Spring		net.ipv4.tcp_retries1)
844*ae57a553SNeil Spring	ip netns exec "$ns_client" sysctl -qw net.ipv4.tcp_retries1=255
845*ae57a553SNeil Spring
846*ae57a553SNeil Spring	local base0 base1
847*ae57a553SNeil Spring	base0=$(link_tx_packets_get "$ns_client" veth0a)
848*ae57a553SNeil Spring	base1=$(link_tx_packets_get "$ns_client" veth1a)
849*ae57a553SNeil Spring
850*ae57a553SNeil Spring	ip netns exec "$ns_client" timeout 15 socat -u \
851*ae57a553SNeil Spring		OPEN:/dev/zero \
852*ae57a553SNeil Spring		"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1]" \
853*ae57a553SNeil Spring		&>/dev/null &
854*ae57a553SNeil Spring	local client_pid=$!
855*ae57a553SNeil Spring
856*ae57a553SNeil Spring	# Wait for enough packets to identify the active path.
857*ae57a553SNeil Spring	# Return 2 for setup failure (distinct from 1 = path changed).
858*ae57a553SNeil Spring	if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \
859*ae57a553SNeil Spring			">= $((base0 + base1 + 50))" \
860*ae57a553SNeil Spring		link_tx_packets_total "$ns_client" > /dev/null; then
861*ae57a553SNeil Spring		ip netns exec "$ns_client" sysctl -qw \
862*ae57a553SNeil Spring			net.ipv4.tcp_retries1="$saved_retries1"
863*ae57a553SNeil Spring		kill "$client_pid" 2>/dev/null
864*ae57a553SNeil Spring		wait "$client_pid" 2>/dev/null
865*ae57a553SNeil Spring		return 2
866*ae57a553SNeil Spring	fi
867*ae57a553SNeil Spring
868*ae57a553SNeil Spring	local mid0 mid1 active_dev inactive_dev
869*ae57a553SNeil Spring	mid0=$(link_tx_packets_get "$ns_client" veth0a)
870*ae57a553SNeil Spring	mid1=$(link_tx_packets_get "$ns_client" veth1a)
871*ae57a553SNeil Spring	if [ $((mid0 - base0)) -ge $((mid1 - base1)) ]; then
872*ae57a553SNeil Spring		active_dev=veth0a; inactive_dev=veth1a
873*ae57a553SNeil Spring	else
874*ae57a553SNeil Spring		active_dev=veth1a; inactive_dev=veth0a
875*ae57a553SNeil Spring	fi
876*ae57a553SNeil Spring
877*ae57a553SNeil Spring	local active_before inactive_before
878*ae57a553SNeil Spring	active_before=$(link_tx_packets_get "$ns_client" "$active_dev")
879*ae57a553SNeil Spring	inactive_before=$(link_tx_packets_get "$ns_client" "$inactive_dev")
880*ae57a553SNeil Spring
881*ae57a553SNeil Spring	# Invalidate the cached dst by bumping the fib6_node sernum.
882*ae57a553SNeil Spring	# Adding and removing a high-metric dummy route achieves this
883*ae57a553SNeil Spring	# without touching the ECMP nexthops, avoiding a transient
884*ae57a553SNeil Spring	# single-nexthop state during multipath route replace.
885*ae57a553SNeil Spring	ip -n "$ns_client" -6 route add fd00:ff::2/128 dev lo metric 9999
886*ae57a553SNeil Spring	ip -n "$ns_client" -6 route del fd00:ff::2/128 dev lo metric 9999
887*ae57a553SNeil Spring
888*ae57a553SNeil Spring	# Wait for enough post-rebuild traffic to detect a path change.
889*ae57a553SNeil Spring	if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \
890*ae57a553SNeil Spring			">= $((active_before + inactive_before + 50))" \
891*ae57a553SNeil Spring		link_tx_packets_total "$ns_client" > /dev/null; then
892*ae57a553SNeil Spring		ip netns exec "$ns_client" sysctl -qw \
893*ae57a553SNeil Spring			net.ipv4.tcp_retries1="$saved_retries1"
894*ae57a553SNeil Spring		kill "$client_pid" 2>/dev/null
895*ae57a553SNeil Spring		wait "$client_pid" 2>/dev/null
896*ae57a553SNeil Spring		return 2
897*ae57a553SNeil Spring	fi
898*ae57a553SNeil Spring
899*ae57a553SNeil Spring	local active_after inactive_after
900*ae57a553SNeil Spring	active_after=$(link_tx_packets_get "$ns_client" "$active_dev")
901*ae57a553SNeil Spring	inactive_after=$(link_tx_packets_get "$ns_client" "$inactive_dev")
902*ae57a553SNeil Spring
903*ae57a553SNeil Spring	local active_delta=$((active_after - active_before))
904*ae57a553SNeil Spring	local inactive_delta=$((inactive_after - inactive_before))
905*ae57a553SNeil Spring
906*ae57a553SNeil Spring	if [ "$inactive_delta" -gt "$active_delta" ]; then
907*ae57a553SNeil Spring		rc=1
908*ae57a553SNeil Spring	fi
909*ae57a553SNeil Spring
910*ae57a553SNeil Spring	ip netns exec "$ns_client" sysctl -qw \
911*ae57a553SNeil Spring		net.ipv4.tcp_retries1="$saved_retries1"
912*ae57a553SNeil Spring	kill "$client_pid" 2>/dev/null
913*ae57a553SNeil Spring	wait "$client_pid" 2>/dev/null
914*ae57a553SNeil Spring	return "$rc"
915*ae57a553SNeil Spring}
916*ae57a553SNeil Spring
917*ae57a553SNeil Spring# Run ecmp_dst_rebuild_check for ECMP_REBUILD_ROUNDS rounds, each with
918*ae57a553SNeil Spring# a fresh server and connection.  With a correct kernel the path is
919*ae57a553SNeil Spring# deterministic (same txhash always selects the same ECMP nexthop),
920*ae57a553SNeil Spring# so any path change is a bug.  Multiple rounds catch a buggy kernel
921*ae57a553SNeil Spring# that picks a random path: each round has 50% chance of accidentally
922*ae57a553SNeil Spring# matching, so 10 rounds gives < 0.1% false-pass probability.
923*ae57a553SNeil Springecmp_dst_rebuild_loop()
924*ae57a553SNeil Spring{
925*ae57a553SNeil Spring	local base_port=$1; shift
926*ae57a553SNeil Spring	local label=$1; shift
927*ae57a553SNeil Spring	local path_changes=0
928*ae57a553SNeil Spring	local r
929*ae57a553SNeil Spring
930*ae57a553SNeil Spring	for r in $(seq 1 "$ECMP_REBUILD_ROUNDS"); do
931*ae57a553SNeil Spring		local port=$((base_port + r - 1))
932*ae57a553SNeil Spring
933*ae57a553SNeil Spring		ip netns exec "$NS2" socat -u \
934*ae57a553SNeil Spring			"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" \
935*ae57a553SNeil Spring			- >/dev/null &
936*ae57a553SNeil Spring		local server_pid=$!
937*ae57a553SNeil Spring
938*ae57a553SNeil Spring		wait_local_port_listen "$NS2" "$port" tcp
939*ae57a553SNeil Spring
940*ae57a553SNeil Spring		local check_rc=0
941*ae57a553SNeil Spring		ecmp_dst_rebuild_check "$NS1" "$port" || check_rc=$?
942*ae57a553SNeil Spring
943*ae57a553SNeil Spring		kill "$server_pid" 2>/dev/null
944*ae57a553SNeil Spring		wait "$server_pid" 2>/dev/null
945*ae57a553SNeil Spring
946*ae57a553SNeil Spring		busywait "$BUSYWAIT_TIMEOUT" \
947*ae57a553SNeil Spring			port_has_no_active_tcp "$NS1" "$port" > /dev/null
948*ae57a553SNeil Spring		busywait "$BUSYWAIT_TIMEOUT" \
949*ae57a553SNeil Spring			port_has_no_active_tcp "$NS2" "$port" > /dev/null
950*ae57a553SNeil Spring
951*ae57a553SNeil Spring		if [ "$check_rc" -eq 2 ]; then
952*ae57a553SNeil Spring			check_err 1 "no TX activity in round $r"
953*ae57a553SNeil Spring			break
954*ae57a553SNeil Spring		elif [ "$check_rc" -eq 1 ]; then
955*ae57a553SNeil Spring			path_changes=$((path_changes + 1))
956*ae57a553SNeil Spring		fi
957*ae57a553SNeil Spring	done
958*ae57a553SNeil Spring
959*ae57a553SNeil Spring	if [ "$path_changes" -gt 0 ]; then
960*ae57a553SNeil Spring		check_err 1 "$path_changes/$ECMP_REBUILD_ROUNDS changed path"
961*ae57a553SNeil Spring	fi
962*ae57a553SNeil Spring
963*ae57a553SNeil Spring	log_test "$label"
964*ae57a553SNeil Spring}
965*ae57a553SNeil Spring
966*ae57a553SNeil Spring# Verify that a dst invalidation does not cause the connection to
967*ae57a553SNeil Spring# switch ECMP paths.  With the fix, both the initial route lookup
968*ae57a553SNeil Spring# (tcp_v6_connect) and subsequent rebuilds (inet6_csk_route_socket)
969*ae57a553SNeil Spring# use sk_txhash >> 1, so the path is stable.
970*ae57a553SNeil Springtest_ecmp_dst_rebuild_consistency()
971*ae57a553SNeil Spring{
972*ae57a553SNeil Spring	RET=0
973*ae57a553SNeil Spring	local base_port
974*ae57a553SNeil Spring	alloc_ports base_port "$ECMP_REBUILD_ROUNDS"
975*ae57a553SNeil Spring
976*ae57a553SNeil Spring	ecmp_dst_rebuild_loop "$base_port" \
977*ae57a553SNeil Spring		"ECMP path stable after dst invalidation"
978*ae57a553SNeil Spring}
979*ae57a553SNeil Spring
980*ae57a553SNeil Spring# Return 0 (true) when no active TCP sockets remain on a port.
981*ae57a553SNeil Spring# TIME_WAIT is excluded because it does not generate outgoing traffic.
982*ae57a553SNeil Springport_has_no_active_tcp()
983*ae57a553SNeil Spring{
984*ae57a553SNeil Spring	local ns=$1; shift
985*ae57a553SNeil Spring	local port=$1; shift
986*ae57a553SNeil Spring
987*ae57a553SNeil Spring	! ip netns exec "$ns" ss -tnH \
988*ae57a553SNeil Spring		state established \
989*ae57a553SNeil Spring		state fin-wait-1 \
990*ae57a553SNeil Spring		state fin-wait-2 \
991*ae57a553SNeil Spring		state close-wait \
992*ae57a553SNeil Spring		state last-ack \
993*ae57a553SNeil Spring		state closing \
994*ae57a553SNeil Spring		state syn-sent \
995*ae57a553SNeil Spring		state syn-recv \
996*ae57a553SNeil Spring		"sport = :$port or dport = :$port" | grep -q .
997*ae57a553SNeil Spring}
998*ae57a553SNeil Spring
999*ae57a553SNeil Spring# Count TCP packets on server egress without blocking them.
1000*ae57a553SNeil Spring# Uses tc filters with "action ok" so packets are counted and passed.
1001*ae57a553SNeil Springcount_tcp()
1002*ae57a553SNeil Spring{
1003*ae57a553SNeil Spring	local ns=$1; shift
1004*ae57a553SNeil Spring	local dev=$1; shift
1005*ae57a553SNeil Spring
1006*ae57a553SNeil Spring	ip netns exec "$ns" tc qdisc add dev "$dev" root handle 1: prio
1007*ae57a553SNeil Spring	ip netns exec "$ns" tc filter add dev "$dev" parent 1: \
1008*ae57a553SNeil Spring		protocol ipv6 prio 1 u32 match u8 0x06 0xff at 6 action ok
1009*ae57a553SNeil Spring}
1010*ae57a553SNeil Spring
1011*ae57a553SNeil Spring# Verify that the server's SYN-ACK (sent from the request socket) and
1012*ae57a553SNeil Spring# subsequent ACKs (sent from the full socket created in cookie_v6_check)
1013*ae57a553SNeil Spring# use the same ECMP path.  With syncookies the request socket is freed
1014*ae57a553SNeil Spring# after the SYN-ACK and a new one is created during cookie validation;
1015*ae57a553SNeil Spring# this test catches the case where the two request sockets pick
1016*ae57a553SNeil Spring# different ECMP paths due to independent txhash values.
1017*ae57a553SNeil Springtest_ecmp_syncookie_path_consistency()
1018*ae57a553SNeil Spring{
1019*ae57a553SNeil Spring	RET=0
1020*ae57a553SNeil Spring
1021*ae57a553SNeil Spring	local saved_syncookies
1022*ae57a553SNeil Spring	saved_syncookies=$(ip netns exec "$NS2" sysctl -n \
1023*ae57a553SNeil Spring		net.ipv4.tcp_syncookies 2>/dev/null)
1024*ae57a553SNeil Spring	if [ -z "$saved_syncookies" ]; then
1025*ae57a553SNeil Spring		log_test_skip "Syncookie server ECMP path consistent"
1026*ae57a553SNeil Spring		return "$ksft_skip"
1027*ae57a553SNeil Spring	fi
1028*ae57a553SNeil Spring	ip netns exec "$NS2" sysctl -qw net.ipv4.tcp_syncookies=2
1029*ae57a553SNeil Spring	defer ip netns exec "$NS2" sysctl -qw \
1030*ae57a553SNeil Spring		net.ipv4.tcp_syncookies="$saved_syncookies"
1031*ae57a553SNeil Spring
1032*ae57a553SNeil Spring	count_tcp "$NS2" veth0b
1033*ae57a553SNeil Spring	defer unblock_tcp "$NS2" veth0b
1034*ae57a553SNeil Spring	count_tcp "$NS2" veth1b
1035*ae57a553SNeil Spring	defer unblock_tcp "$NS2" veth1b
1036*ae57a553SNeil Spring
1037*ae57a553SNeil Spring	local path_splits=0
1038*ae57a553SNeil Spring	local r base_port
1039*ae57a553SNeil Spring	alloc_ports base_port "$ECMP_REBUILD_ROUNDS"
1040*ae57a553SNeil Spring
1041*ae57a553SNeil Spring	for r in $(seq 1 "$ECMP_REBUILD_ROUNDS"); do
1042*ae57a553SNeil Spring		local port=$((base_port + r - 1))
1043*ae57a553SNeil Spring
1044*ae57a553SNeil Spring		ip netns exec "$NS2" socat -u \
1045*ae57a553SNeil Spring			"TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" \
1046*ae57a553SNeil Spring			- >/dev/null &
1047*ae57a553SNeil Spring		local server_pid=$!
1048*ae57a553SNeil Spring
1049*ae57a553SNeil Spring		wait_local_port_listen "$NS2" "$port" tcp
1050*ae57a553SNeil Spring
1051*ae57a553SNeil Spring		local srv_base0 srv_base1
1052*ae57a553SNeil Spring		srv_base0=$(tc_filter_pkt_count "$NS2" veth0b)
1053*ae57a553SNeil Spring		srv_base1=$(tc_filter_pkt_count "$NS2" veth1b)
1054*ae57a553SNeil Spring
1055*ae57a553SNeil Spring		ip netns exec "$NS1" timeout 5 socat -u \
1056*ae57a553SNeil Spring			OPEN:/dev/zero \
1057*ae57a553SNeil Spring			"TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1]" \
1058*ae57a553SNeil Spring			&>/dev/null &
1059*ae57a553SNeil Spring		local client_pid=$!
1060*ae57a553SNeil Spring
1061*ae57a553SNeil Spring		local cli_base
1062*ae57a553SNeil Spring		cli_base=$(link_tx_packets_total "$NS1")
1063*ae57a553SNeil Spring		if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \
1064*ae57a553SNeil Spring				">= $((cli_base + 200))" \
1065*ae57a553SNeil Spring			link_tx_packets_total "$NS1" > /dev/null; then
1066*ae57a553SNeil Spring			check_err 1 "no TX activity in round $r"
1067*ae57a553SNeil Spring			kill "$client_pid" 2>/dev/null
1068*ae57a553SNeil Spring			wait "$client_pid" 2>/dev/null
1069*ae57a553SNeil Spring			kill "$server_pid" 2>/dev/null
1070*ae57a553SNeil Spring			wait "$server_pid" 2>/dev/null
1071*ae57a553SNeil Spring			break
1072*ae57a553SNeil Spring		fi
1073*ae57a553SNeil Spring
1074*ae57a553SNeil Spring		local srv_tcp0 srv_tcp1
1075*ae57a553SNeil Spring		srv_tcp0=$(tc_filter_pkt_count "$NS2" veth0b)
1076*ae57a553SNeil Spring		srv_tcp1=$(tc_filter_pkt_count "$NS2" veth1b)
1077*ae57a553SNeil Spring		local srv_delta0=$(( ${srv_tcp0:-0} - ${srv_base0:-0} ))
1078*ae57a553SNeil Spring		local srv_delta1=$(( ${srv_tcp1:-0} - ${srv_base1:-0} ))
1079*ae57a553SNeil Spring
1080*ae57a553SNeil Spring		if [ "$srv_delta0" -gt 0 ] && [ "$srv_delta1" -gt 0 ]; then
1081*ae57a553SNeil Spring			path_splits=$((path_splits + 1))
1082*ae57a553SNeil Spring		fi
1083*ae57a553SNeil Spring
1084*ae57a553SNeil Spring		kill "$client_pid" 2>/dev/null
1085*ae57a553SNeil Spring		wait "$client_pid" 2>/dev/null
1086*ae57a553SNeil Spring		kill "$server_pid" 2>/dev/null
1087*ae57a553SNeil Spring		wait "$server_pid" 2>/dev/null
1088*ae57a553SNeil Spring
1089*ae57a553SNeil Spring		# Wait for TCP teardown packets (FIN/RST) to finish so
1090*ae57a553SNeil Spring		# they do not pollute the next round's tc filter counters.
1091*ae57a553SNeil Spring		busywait "$BUSYWAIT_TIMEOUT" \
1092*ae57a553SNeil Spring			port_has_no_active_tcp "$NS1" "$port" > /dev/null
1093*ae57a553SNeil Spring		busywait "$BUSYWAIT_TIMEOUT" \
1094*ae57a553SNeil Spring			port_has_no_active_tcp "$NS2" "$port" > /dev/null
1095*ae57a553SNeil Spring	done
1096*ae57a553SNeil Spring
1097*ae57a553SNeil Spring	if [ "$path_splits" -gt 0 ]; then
1098*ae57a553SNeil Spring		check_err 1 "$path_splits/$ECMP_REBUILD_ROUNDS had split server path"
1099*ae57a553SNeil Spring	fi
1100*ae57a553SNeil Spring
1101*ae57a553SNeil Spring	log_test "Syncookie server ECMP path consistent"
1102*ae57a553SNeil Spring}
1103*ae57a553SNeil Spring
1104*ae57a553SNeil Springrequire_command socat
1105*ae57a553SNeil Spring
1106*ae57a553SNeil Springtrap 'defer_scopes_cleanup; cleanup_all_ns' EXIT
1107*ae57a553SNeil Springsetup || exit $?
1108*ae57a553SNeil Springtests_run
1109*ae57a553SNeil Springexit "$EXIT_STATUS"
1110