1#!/bin/bash
2# SPDX-License-Identifier: GPL-2.0
3
4##############################################################################
5# Defines
6
7# Can be overridden by the configuration file.
8PING=${PING:=ping}
9PING6=${PING6:=ping6}
10MZ=${MZ:=mausezahn}
11WAIT_TIME=${WAIT_TIME:=5}
12PAUSE_ON_FAIL=${PAUSE_ON_FAIL:=no}
13PAUSE_ON_CLEANUP=${PAUSE_ON_CLEANUP:=no}
14NETIF_TYPE=${NETIF_TYPE:=veth}
15NETIF_CREATE=${NETIF_CREATE:=yes}
16
17relative_path="${BASH_SOURCE%/*}"
18if [[ "$relative_path" == "${BASH_SOURCE}" ]]; then
19	relative_path="."
20fi
21
22if [[ -f $relative_path/forwarding.config ]]; then
23	source "$relative_path/forwarding.config"
24fi
25
26##############################################################################
27# Sanity checks
28
29check_tc_version()
30{
31	tc -j &> /dev/null
32	if [[ $? -ne 0 ]]; then
33		echo "SKIP: iproute2 too old; tc is missing JSON support"
34		exit 1
35	fi
36}
37
38check_tc_shblock_support()
39{
40	tc filter help 2>&1 | grep block &> /dev/null
41	if [[ $? -ne 0 ]]; then
42		echo "SKIP: iproute2 too old; tc is missing shared block support"
43		exit 1
44	fi
45}
46
47check_tc_chain_support()
48{
49	tc help 2>&1|grep chain &> /dev/null
50	if [[ $? -ne 0 ]]; then
51		echo "SKIP: iproute2 too old; tc is missing chain support"
52		exit 1
53	fi
54}
55
56if [[ "$(id -u)" -ne 0 ]]; then
57	echo "SKIP: need root privileges"
58	exit 0
59fi
60
61if [[ "$CHECK_TC" = "yes" ]]; then
62	check_tc_version
63fi
64
65if [[ ! -x "$(command -v jq)" ]]; then
66	echo "SKIP: jq not installed"
67	exit 1
68fi
69
70if [[ ! -x "$(command -v $MZ)" ]]; then
71	echo "SKIP: $MZ not installed"
72	exit 1
73fi
74
75if [[ ! -v NUM_NETIFS ]]; then
76	echo "SKIP: importer does not define \"NUM_NETIFS\""
77	exit 1
78fi
79
80##############################################################################
81# Command line options handling
82
83count=0
84
85while [[ $# -gt 0 ]]; do
86	if [[ "$count" -eq "0" ]]; then
87		unset NETIFS
88		declare -A NETIFS
89	fi
90	count=$((count + 1))
91	NETIFS[p$count]="$1"
92	shift
93done
94
95##############################################################################
96# Network interfaces configuration
97
98create_netif_veth()
99{
100	local i
101
102	for i in $(eval echo {1..$NUM_NETIFS}); do
103		local j=$((i+1))
104
105		ip link show dev ${NETIFS[p$i]} &> /dev/null
106		if [[ $? -ne 0 ]]; then
107			ip link add ${NETIFS[p$i]} type veth \
108				peer name ${NETIFS[p$j]}
109			if [[ $? -ne 0 ]]; then
110				echo "Failed to create netif"
111				exit 1
112			fi
113		fi
114		i=$j
115	done
116}
117
118create_netif()
119{
120	case "$NETIF_TYPE" in
121	veth) create_netif_veth
122	      ;;
123	*) echo "Can not create interfaces of type \'$NETIF_TYPE\'"
124	   exit 1
125	   ;;
126	esac
127}
128
129if [[ "$NETIF_CREATE" = "yes" ]]; then
130	create_netif
131fi
132
133for i in $(eval echo {1..$NUM_NETIFS}); do
134	ip link show dev ${NETIFS[p$i]} &> /dev/null
135	if [[ $? -ne 0 ]]; then
136		echo "SKIP: could not find all required interfaces"
137		exit 1
138	fi
139done
140
141##############################################################################
142# Helpers
143
144# Exit status to return at the end. Set in case one of the tests fails.
145EXIT_STATUS=0
146# Per-test return value. Clear at the beginning of each test.
147RET=0
148
149check_err()
150{
151	local err=$1
152	local msg=$2
153
154	if [[ $RET -eq 0 && $err -ne 0 ]]; then
155		RET=$err
156		retmsg=$msg
157	fi
158}
159
160check_fail()
161{
162	local err=$1
163	local msg=$2
164
165	if [[ $RET -eq 0 && $err -eq 0 ]]; then
166		RET=1
167		retmsg=$msg
168	fi
169}
170
171check_err_fail()
172{
173	local should_fail=$1; shift
174	local err=$1; shift
175	local what=$1; shift
176
177	if ((should_fail)); then
178		check_fail $err "$what succeeded, but should have failed"
179	else
180		check_err $err "$what failed"
181	fi
182}
183
184log_test()
185{
186	local test_name=$1
187	local opt_str=$2
188
189	if [[ $# -eq 2 ]]; then
190		opt_str="($opt_str)"
191	fi
192
193	if [[ $RET -ne 0 ]]; then
194		EXIT_STATUS=1
195		printf "TEST: %-60s  [FAIL]\n" "$test_name $opt_str"
196		if [[ ! -z "$retmsg" ]]; then
197			printf "\t%s\n" "$retmsg"
198		fi
199		if [ "${PAUSE_ON_FAIL}" = "yes" ]; then
200			echo "Hit enter to continue, 'q' to quit"
201			read a
202			[ "$a" = "q" ] && exit 1
203		fi
204		return 1
205	fi
206
207	printf "TEST: %-60s  [PASS]\n" "$test_name $opt_str"
208	return 0
209}
210
211log_info()
212{
213	local msg=$1
214
215	echo "INFO: $msg"
216}
217
218setup_wait_dev()
219{
220	local dev=$1; shift
221
222	while true; do
223		ip link show dev $dev up \
224			| grep 'state UP' &> /dev/null
225		if [[ $? -ne 0 ]]; then
226			sleep 1
227		else
228			break
229		fi
230	done
231}
232
233setup_wait()
234{
235	local num_netifs=${1:-$NUM_NETIFS}
236
237	for ((i = 1; i <= num_netifs; ++i)); do
238		setup_wait_dev ${NETIFS[p$i]}
239	done
240
241	# Make sure links are ready.
242	sleep $WAIT_TIME
243}
244
245pre_cleanup()
246{
247	if [ "${PAUSE_ON_CLEANUP}" = "yes" ]; then
248		echo "Pausing before cleanup, hit any key to continue"
249		read
250	fi
251}
252
253vrf_prepare()
254{
255	ip -4 rule add pref 32765 table local
256	ip -4 rule del pref 0
257	ip -6 rule add pref 32765 table local
258	ip -6 rule del pref 0
259}
260
261vrf_cleanup()
262{
263	ip -6 rule add pref 0 table local
264	ip -6 rule del pref 32765
265	ip -4 rule add pref 0 table local
266	ip -4 rule del pref 32765
267}
268
269__last_tb_id=0
270declare -A __TB_IDS
271
272__vrf_td_id_assign()
273{
274	local vrf_name=$1
275
276	__last_tb_id=$((__last_tb_id + 1))
277	__TB_IDS[$vrf_name]=$__last_tb_id
278	return $__last_tb_id
279}
280
281__vrf_td_id_lookup()
282{
283	local vrf_name=$1
284
285	return ${__TB_IDS[$vrf_name]}
286}
287
288vrf_create()
289{
290	local vrf_name=$1
291	local tb_id
292
293	__vrf_td_id_assign $vrf_name
294	tb_id=$?
295
296	ip link add dev $vrf_name type vrf table $tb_id
297	ip -4 route add table $tb_id unreachable default metric 4278198272
298	ip -6 route add table $tb_id unreachable default metric 4278198272
299}
300
301vrf_destroy()
302{
303	local vrf_name=$1
304	local tb_id
305
306	__vrf_td_id_lookup $vrf_name
307	tb_id=$?
308
309	ip -6 route del table $tb_id unreachable default metric 4278198272
310	ip -4 route del table $tb_id unreachable default metric 4278198272
311	ip link del dev $vrf_name
312}
313
314__addr_add_del()
315{
316	local if_name=$1
317	local add_del=$2
318	local array
319
320	shift
321	shift
322	array=("${@}")
323
324	for addrstr in "${array[@]}"; do
325		ip address $add_del $addrstr dev $if_name
326	done
327}
328
329__simple_if_init()
330{
331	local if_name=$1; shift
332	local vrf_name=$1; shift
333	local addrs=("${@}")
334
335	ip link set dev $if_name master $vrf_name
336	ip link set dev $if_name up
337
338	__addr_add_del $if_name add "${addrs[@]}"
339}
340
341__simple_if_fini()
342{
343	local if_name=$1; shift
344	local addrs=("${@}")
345
346	__addr_add_del $if_name del "${addrs[@]}"
347
348	ip link set dev $if_name down
349	ip link set dev $if_name nomaster
350}
351
352simple_if_init()
353{
354	local if_name=$1
355	local vrf_name
356	local array
357
358	shift
359	vrf_name=v$if_name
360	array=("${@}")
361
362	vrf_create $vrf_name
363	ip link set dev $vrf_name up
364	__simple_if_init $if_name $vrf_name "${array[@]}"
365}
366
367simple_if_fini()
368{
369	local if_name=$1
370	local vrf_name
371	local array
372
373	shift
374	vrf_name=v$if_name
375	array=("${@}")
376
377	__simple_if_fini $if_name "${array[@]}"
378	vrf_destroy $vrf_name
379}
380
381tunnel_create()
382{
383	local name=$1; shift
384	local type=$1; shift
385	local local=$1; shift
386	local remote=$1; shift
387
388	ip link add name $name type $type \
389	   local $local remote $remote "$@"
390	ip link set dev $name up
391}
392
393tunnel_destroy()
394{
395	local name=$1; shift
396
397	ip link del dev $name
398}
399
400vlan_create()
401{
402	local if_name=$1; shift
403	local vid=$1; shift
404	local vrf=$1; shift
405	local ips=("${@}")
406	local name=$if_name.$vid
407
408	ip link add name $name link $if_name type vlan id $vid
409	if [ "$vrf" != "" ]; then
410		ip link set dev $name master $vrf
411	fi
412	ip link set dev $name up
413	__addr_add_del $name add "${ips[@]}"
414}
415
416vlan_destroy()
417{
418	local if_name=$1; shift
419	local vid=$1; shift
420	local name=$if_name.$vid
421
422	ip link del dev $name
423}
424
425master_name_get()
426{
427	local if_name=$1
428
429	ip -j link show dev $if_name | jq -r '.[]["master"]'
430}
431
432link_stats_tx_packets_get()
433{
434       local if_name=$1
435
436       ip -j -s link show dev $if_name | jq '.[]["stats64"]["tx"]["packets"]'
437}
438
439tc_rule_stats_get()
440{
441	local dev=$1; shift
442	local pref=$1; shift
443	local dir=$1; shift
444
445	tc -j -s filter show dev $dev ${dir:-ingress} pref $pref \
446	    | jq '.[1].options.actions[].stats.packets'
447}
448
449mac_get()
450{
451	local if_name=$1
452
453	ip -j link show dev $if_name | jq -r '.[]["address"]'
454}
455
456bridge_ageing_time_get()
457{
458	local bridge=$1
459	local ageing_time
460
461	# Need to divide by 100 to convert to seconds.
462	ageing_time=$(ip -j -d link show dev $bridge \
463		      | jq '.[]["linkinfo"]["info_data"]["ageing_time"]')
464	echo $((ageing_time / 100))
465}
466
467declare -A SYSCTL_ORIG
468sysctl_set()
469{
470	local key=$1; shift
471	local value=$1; shift
472
473	SYSCTL_ORIG[$key]=$(sysctl -n $key)
474	sysctl -qw $key=$value
475}
476
477sysctl_restore()
478{
479	local key=$1; shift
480
481	sysctl -qw $key=${SYSCTL_ORIG["$key"]}
482}
483
484forwarding_enable()
485{
486	sysctl_set net.ipv4.conf.all.forwarding 1
487	sysctl_set net.ipv6.conf.all.forwarding 1
488}
489
490forwarding_restore()
491{
492	sysctl_restore net.ipv6.conf.all.forwarding
493	sysctl_restore net.ipv4.conf.all.forwarding
494}
495
496tc_offload_check()
497{
498	local num_netifs=${1:-$NUM_NETIFS}
499
500	for ((i = 1; i <= num_netifs; ++i)); do
501		ethtool -k ${NETIFS[p$i]} \
502			| grep "hw-tc-offload: on" &> /dev/null
503		if [[ $? -ne 0 ]]; then
504			return 1
505		fi
506	done
507
508	return 0
509}
510
511trap_install()
512{
513	local dev=$1; shift
514	local direction=$1; shift
515
516	# Some devices may not support or need in-hardware trapping of traffic
517	# (e.g. the veth pairs that this library creates for non-existent
518	# loopbacks). Use continue instead, so that there is a filter in there
519	# (some tests check counters), and so that other filters are still
520	# processed.
521	tc filter add dev $dev $direction pref 1 \
522		flower skip_sw action trap 2>/dev/null \
523	    || tc filter add dev $dev $direction pref 1 \
524		       flower action continue
525}
526
527trap_uninstall()
528{
529	local dev=$1; shift
530	local direction=$1; shift
531
532	tc filter del dev $dev $direction pref 1 flower
533}
534
535slow_path_trap_install()
536{
537	# For slow-path testing, we need to install a trap to get to
538	# slow path the packets that would otherwise be switched in HW.
539	if [ "${tcflags/skip_hw}" != "$tcflags" ]; then
540		trap_install "$@"
541	fi
542}
543
544slow_path_trap_uninstall()
545{
546	if [ "${tcflags/skip_hw}" != "$tcflags" ]; then
547		trap_uninstall "$@"
548	fi
549}
550
551__icmp_capture_add_del()
552{
553	local add_del=$1; shift
554	local pref=$1; shift
555	local vsuf=$1; shift
556	local tundev=$1; shift
557	local filter=$1; shift
558
559	tc filter $add_del dev "$tundev" ingress \
560	   proto ip$vsuf pref $pref \
561	   flower ip_proto icmp$vsuf $filter \
562	   action pass
563}
564
565icmp_capture_install()
566{
567	__icmp_capture_add_del add 100 "" "$@"
568}
569
570icmp_capture_uninstall()
571{
572	__icmp_capture_add_del del 100 "" "$@"
573}
574
575icmp6_capture_install()
576{
577	__icmp_capture_add_del add 100 v6 "$@"
578}
579
580icmp6_capture_uninstall()
581{
582	__icmp_capture_add_del del 100 v6 "$@"
583}
584
585__vlan_capture_add_del()
586{
587	local add_del=$1; shift
588	local pref=$1; shift
589	local dev=$1; shift
590	local filter=$1; shift
591
592	tc filter $add_del dev "$dev" ingress \
593	   proto 802.1q pref $pref \
594	   flower $filter \
595	   action pass
596}
597
598vlan_capture_install()
599{
600	__vlan_capture_add_del add 100 "$@"
601}
602
603vlan_capture_uninstall()
604{
605	__vlan_capture_add_del del 100 "$@"
606}
607
608matchall_sink_create()
609{
610	local dev=$1; shift
611
612	tc qdisc add dev $dev clsact
613	tc filter add dev $dev ingress \
614	   pref 10000 \
615	   matchall \
616	   action drop
617}
618
619tests_run()
620{
621	local current_test
622
623	for current_test in ${TESTS:-$ALL_TESTS}; do
624		$current_test
625	done
626}
627
628multipath_eval()
629{
630	local desc="$1"
631	local weight_rp12=$2
632	local weight_rp13=$3
633	local packets_rp12=$4
634	local packets_rp13=$5
635	local weights_ratio packets_ratio diff
636
637	RET=0
638
639	if [[ "$weight_rp12" -gt "$weight_rp13" ]]; then
640		weights_ratio=$(echo "scale=2; $weight_rp12 / $weight_rp13" \
641				| bc -l)
642	else
643		weights_ratio=$(echo "scale=2; $weight_rp13 / $weight_rp12" \
644				| bc -l)
645	fi
646
647	if [[ "$packets_rp12" -eq "0" || "$packets_rp13" -eq "0" ]]; then
648	       check_err 1 "Packet difference is 0"
649	       log_test "Multipath"
650	       log_info "Expected ratio $weights_ratio"
651	       return
652	fi
653
654	if [[ "$weight_rp12" -gt "$weight_rp13" ]]; then
655		packets_ratio=$(echo "scale=2; $packets_rp12 / $packets_rp13" \
656				| bc -l)
657	else
658		packets_ratio=$(echo "scale=2; $packets_rp13 / $packets_rp12" \
659				| bc -l)
660	fi
661
662	diff=$(echo $weights_ratio - $packets_ratio | bc -l)
663	diff=${diff#-}
664
665	test "$(echo "$diff / $weights_ratio > 0.15" | bc -l)" -eq 0
666	check_err $? "Too large discrepancy between expected and measured ratios"
667	log_test "$desc"
668	log_info "Expected ratio $weights_ratio Measured ratio $packets_ratio"
669}
670
671##############################################################################
672# Tests
673
674ping_do()
675{
676	local if_name=$1
677	local dip=$2
678	local vrf_name
679
680	vrf_name=$(master_name_get $if_name)
681	ip vrf exec $vrf_name $PING $dip -c 10 -i 0.1 -w 2 &> /dev/null
682}
683
684ping_test()
685{
686	RET=0
687
688	ping_do $1 $2
689	check_err $?
690	log_test "ping"
691}
692
693ping6_do()
694{
695	local if_name=$1
696	local dip=$2
697	local vrf_name
698
699	vrf_name=$(master_name_get $if_name)
700	ip vrf exec $vrf_name $PING6 $dip -c 10 -i 0.1 -w 2 &> /dev/null
701}
702
703ping6_test()
704{
705	RET=0
706
707	ping6_do $1 $2
708	check_err $?
709	log_test "ping6"
710}
711
712learning_test()
713{
714	local bridge=$1
715	local br_port1=$2	# Connected to `host1_if`.
716	local host1_if=$3
717	local host2_if=$4
718	local mac=de:ad:be:ef:13:37
719	local ageing_time
720
721	RET=0
722
723	bridge -j fdb show br $bridge brport $br_port1 \
724		| jq -e ".[] | select(.mac == \"$mac\")" &> /dev/null
725	check_fail $? "Found FDB record when should not"
726
727	# Disable unknown unicast flooding on `br_port1` to make sure
728	# packets are only forwarded through the port after a matching
729	# FDB entry was installed.
730	bridge link set dev $br_port1 flood off
731
732	tc qdisc add dev $host1_if ingress
733	tc filter add dev $host1_if ingress protocol ip pref 1 handle 101 \
734		flower dst_mac $mac action drop
735
736	$MZ $host2_if -c 1 -p 64 -b $mac -t ip -q
737	sleep 1
738
739	tc -j -s filter show dev $host1_if ingress \
740		| jq -e ".[] | select(.options.handle == 101) \
741		| select(.options.actions[0].stats.packets == 1)" &> /dev/null
742	check_fail $? "Packet reached second host when should not"
743
744	$MZ $host1_if -c 1 -p 64 -a $mac -t ip -q
745	sleep 1
746
747	bridge -j fdb show br $bridge brport $br_port1 \
748		| jq -e ".[] | select(.mac == \"$mac\")" &> /dev/null
749	check_err $? "Did not find FDB record when should"
750
751	$MZ $host2_if -c 1 -p 64 -b $mac -t ip -q
752	sleep 1
753
754	tc -j -s filter show dev $host1_if ingress \
755		| jq -e ".[] | select(.options.handle == 101) \
756		| select(.options.actions[0].stats.packets == 1)" &> /dev/null
757	check_err $? "Packet did not reach second host when should"
758
759	# Wait for 10 seconds after the ageing time to make sure FDB
760	# record was aged-out.
761	ageing_time=$(bridge_ageing_time_get $bridge)
762	sleep $((ageing_time + 10))
763
764	bridge -j fdb show br $bridge brport $br_port1 \
765		| jq -e ".[] | select(.mac == \"$mac\")" &> /dev/null
766	check_fail $? "Found FDB record when should not"
767
768	bridge link set dev $br_port1 learning off
769
770	$MZ $host1_if -c 1 -p 64 -a $mac -t ip -q
771	sleep 1
772
773	bridge -j fdb show br $bridge brport $br_port1 \
774		| jq -e ".[] | select(.mac == \"$mac\")" &> /dev/null
775	check_fail $? "Found FDB record when should not"
776
777	bridge link set dev $br_port1 learning on
778
779	tc filter del dev $host1_if ingress protocol ip pref 1 handle 101 flower
780	tc qdisc del dev $host1_if ingress
781
782	bridge link set dev $br_port1 flood on
783
784	log_test "FDB learning"
785}
786
787flood_test_do()
788{
789	local should_flood=$1
790	local mac=$2
791	local ip=$3
792	local host1_if=$4
793	local host2_if=$5
794	local err=0
795
796	# Add an ACL on `host2_if` which will tell us whether the packet
797	# was flooded to it or not.
798	tc qdisc add dev $host2_if ingress
799	tc filter add dev $host2_if ingress protocol ip pref 1 handle 101 \
800		flower dst_mac $mac action drop
801
802	$MZ $host1_if -c 1 -p 64 -b $mac -B $ip -t ip -q
803	sleep 1
804
805	tc -j -s filter show dev $host2_if ingress \
806		| jq -e ".[] | select(.options.handle == 101) \
807		| select(.options.actions[0].stats.packets == 1)" &> /dev/null
808	if [[ $? -ne 0 && $should_flood == "true" || \
809	      $? -eq 0 && $should_flood == "false" ]]; then
810		err=1
811	fi
812
813	tc filter del dev $host2_if ingress protocol ip pref 1 handle 101 flower
814	tc qdisc del dev $host2_if ingress
815
816	return $err
817}
818
819flood_unicast_test()
820{
821	local br_port=$1
822	local host1_if=$2
823	local host2_if=$3
824	local mac=de:ad:be:ef:13:37
825	local ip=192.0.2.100
826
827	RET=0
828
829	bridge link set dev $br_port flood off
830
831	flood_test_do false $mac $ip $host1_if $host2_if
832	check_err $? "Packet flooded when should not"
833
834	bridge link set dev $br_port flood on
835
836	flood_test_do true $mac $ip $host1_if $host2_if
837	check_err $? "Packet was not flooded when should"
838
839	log_test "Unknown unicast flood"
840}
841
842flood_multicast_test()
843{
844	local br_port=$1
845	local host1_if=$2
846	local host2_if=$3
847	local mac=01:00:5e:00:00:01
848	local ip=239.0.0.1
849
850	RET=0
851
852	bridge link set dev $br_port mcast_flood off
853
854	flood_test_do false $mac $ip $host1_if $host2_if
855	check_err $? "Packet flooded when should not"
856
857	bridge link set dev $br_port mcast_flood on
858
859	flood_test_do true $mac $ip $host1_if $host2_if
860	check_err $? "Packet was not flooded when should"
861
862	log_test "Unregistered multicast flood"
863}
864
865flood_test()
866{
867	# `br_port` is connected to `host2_if`
868	local br_port=$1
869	local host1_if=$2
870	local host2_if=$3
871
872	flood_unicast_test $br_port $host1_if $host2_if
873	flood_multicast_test $br_port $host1_if $host2_if
874}
875