#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#shellcheck disable=SC2034 # SC doesn't see our uses of global variables

##############################################################################
# Topology description. p1 looped back to p2, p3 to p4 and so on.

declare -A NETIFS=(
    [p1]=veth0
    [p2]=veth1
    [p3]=veth2
    [p4]=veth3
    [p5]=veth4
    [p6]=veth5
    [p7]=veth6
    [p8]=veth7
    [p9]=veth8
    [p10]=veth9
)

# Port that does not have a cable connected.
: "${NETIF_NO_CABLE:=eth8}"

##############################################################################
# Defines

# Networking utilities.
: "${PING:=ping}"
: "${PING6:=ping6}"	# Some distros just use ping.
: "${ARPING:=arping}"
: "${TROUTE6:=traceroute6}"

# Packet generator.
: "${MZ:=mausezahn}"	# Some distributions use 'mz'.
: "${MZ_DELAY:=0}"

# Host configuration tools.
: "${TEAMD:=teamd}"
: "${MCD:=smcrouted}"
: "${MC_CLI:=smcroutectl}"
: "${MCD_TABLE_NAME:=selftests}"

# Constants for netdevice bring-up:
# Default time in seconds to wait for an interface to come up before giving up
# and bailing out. Used during initial setup.
: "${INTERFACE_TIMEOUT:=600}"
# Like INTERFACE_TIMEOUT, but default for ad-hoc waiting in testing scripts.
: "${WAIT_TIMEOUT:=20}"
# Time to wait after interfaces participating in the test are all UP.
: "${WAIT_TIME:=5}"

# Whether to pause on, respectively, after a failure and before cleanup.
: "${PAUSE_ON_CLEANUP:=no}"

# Whether to create virtual interfaces, and what netdevice type they should be.
: "${NETIF_CREATE:=yes}"
: "${NETIF_TYPE:=veth}"

# Constants for ping tests:
# How many packets should be sent.
: "${PING_COUNT:=10}"
# Timeout (in seconds) before ping exits regardless of how many packets have
# been sent or received
: "${PING_TIMEOUT:=5}"

# Minimum ageing_time (in centiseconds) supported by hardware
: "${LOW_AGEING_TIME:=1000}"

# Whether to check for availability of certain tools.
: "${REQUIRE_JQ:=yes}"
: "${REQUIRE_MZ:=yes}"
: "${REQUIRE_MTOOLS:=no}"
: "${REQUIRE_TEAMD:=no}"

# Whether to override MAC addresses on interfaces participating in the test.
: "${STABLE_MAC_ADDRS:=no}"

# Flags for tcpdump
: "${TCPDUMP_EXTRA_FLAGS:=}"

# Flags for TC filters.
: "${TC_FLAG:=skip_hw}"

# Whether the machine is "slow" -- i.e. might be incapable of running tests
# involving heavy traffic. This might be the case on a debug kernel, a VM, or
# e.g. a low-power board.
: "${KSFT_MACHINE_SLOW:=no}"

##############################################################################
# Find netifs by test-specified driver name

driver_name_get()
{
	local dev=$1; shift
	local driver_path="/sys/class/net/$dev/device/driver"

	if [[ -L $driver_path ]]; then
		basename `realpath $driver_path`
	fi
}

netif_find_driver()
{
	local ifnames=`ip -j link show | jq -r ".[].ifname"`
	local count=0

	for ifname in $ifnames
	do
		local driver_name=`driver_name_get $ifname`
		if [[ ! -z $driver_name && $driver_name == $NETIF_FIND_DRIVER ]]; then
			count=$((count + 1))
			NETIFS[p$count]="$ifname"
		fi
	done
}

# Whether to find netdevice according to the driver speficied by the importer
: "${NETIF_FIND_DRIVER:=}"

if [[ $NETIF_FIND_DRIVER ]]; then
	unset NETIFS
	declare -A NETIFS
	netif_find_driver
fi

net_forwarding_dir=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")

if [[ -f $net_forwarding_dir/forwarding.config ]]; then
	source "$net_forwarding_dir/forwarding.config"
fi

source "$net_forwarding_dir/../lib.sh"

##############################################################################
# Sanity checks

check_tc_version()
{
	tc -j &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc is missing JSON support"
		exit $ksft_skip
	fi
}

check_tc_erspan_support()
{
	local dev=$1; shift

	tc filter add dev $dev ingress pref 1 handle 1 flower \
		erspan_opts 1:0:0:0 &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc is missing erspan support"
		return $ksft_skip
	fi
	tc filter del dev $dev ingress pref 1 handle 1 flower \
		erspan_opts 1:0:0:0 &> /dev/null
}

# Old versions of tc don't understand "mpls_uc"
check_tc_mpls_support()
{
	local dev=$1; shift

	tc filter add dev $dev ingress protocol mpls_uc pref 1 handle 1 \
		matchall action pipe &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc is missing MPLS support"
		return $ksft_skip
	fi
	tc filter del dev $dev ingress protocol mpls_uc pref 1 handle 1 \
		matchall
}

# Old versions of tc produce invalid json output for mpls lse statistics
check_tc_mpls_lse_stats()
{
	local dev=$1; shift
	local ret;

	tc filter add dev $dev ingress protocol mpls_uc pref 1 handle 1 \
		flower mpls lse depth 2                                 \
		action continue &> /dev/null

	if [[ $? -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc-flower is missing extended MPLS support"
		return $ksft_skip
	fi

	tc -j filter show dev $dev ingress protocol mpls_uc | jq . &> /dev/null
	ret=$?
	tc filter del dev $dev ingress protocol mpls_uc pref 1 handle 1 \
		flower

	if [[ $ret -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc-flower produces invalid json output for extended MPLS filters"
		return $ksft_skip
	fi
}

check_tc_shblock_support()
{
	tc filter help 2>&1 | grep block &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc is missing shared block support"
		exit $ksft_skip
	fi
}

check_tc_chain_support()
{
	tc help 2>&1|grep chain &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc is missing chain support"
		exit $ksft_skip
	fi
}

check_tc_action_hw_stats_support()
{
	tc actions help 2>&1 | grep -q hw_stats
	if [[ $? -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc is missing action hw_stats support"
		exit $ksft_skip
	fi
}

check_tc_fp_support()
{
	tc qdisc add dev lo mqprio help 2>&1 | grep -q "fp "
	if [[ $? -ne 0 ]]; then
		echo "SKIP: iproute2 too old; tc is missing frame preemption support"
		exit $ksft_skip
	fi
}

check_ethtool_lanes_support()
{
	ethtool --help 2>&1| grep lanes &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: ethtool too old; it is missing lanes support"
		exit $ksft_skip
	fi
}

check_ethtool_mm_support()
{
	ethtool --help 2>&1| grep -- '--show-mm' &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: ethtool too old; it is missing MAC Merge layer support"
		exit $ksft_skip
	fi
}

check_ethtool_counter_group_support()
{
	ethtool --help 2>&1| grep -- '--all-groups' &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: ethtool too old; it is missing standard counter group support"
		exit $ksft_skip
	fi
}

check_ethtool_pmac_std_stats_support()
{
	local dev=$1; shift
	local grp=$1; shift

	[ 0 -ne $(ethtool --json -S $dev --all-groups --src pmac 2>/dev/null \
		| jq ".[].\"$grp\" | length") ]
}

check_locked_port_support()
{
	if ! bridge -d link show | grep -q " locked"; then
		echo "SKIP: iproute2 too old; Locked port feature not supported."
		return $ksft_skip
	fi
}

check_port_mab_support()
{
	if ! bridge -d link show | grep -q "mab"; then
		echo "SKIP: iproute2 too old; MacAuth feature not supported."
		return $ksft_skip
	fi
}

if [[ "$(id -u)" -ne 0 ]]; then
	echo "SKIP: need root privileges"
	exit $ksft_skip
fi

check_driver()
{
	local dev=$1; shift
	local expected=$1; shift
	local driver_name=`driver_name_get $dev`

	if [[ $driver_name != $expected ]]; then
		echo "SKIP: expected driver $expected for $dev, got $driver_name instead"
		exit $ksft_skip
	fi
}

if [[ "$CHECK_TC" = "yes" ]]; then
	check_tc_version
fi

# IPv6 support was added in v3.0
check_mtools_version()
{
	local version="$(msend -v)"
	local major

	version=${version##msend version }
	major=$(echo $version | cut -d. -f1)

	if [ $major -lt 3 ]; then
		echo "SKIP: expected mtools version 3.0, got $version"
		exit $ksft_skip
	fi
}

if [[ "$REQUIRE_JQ" = "yes" ]]; then
	require_command jq
fi
if [[ "$REQUIRE_MZ" = "yes" ]]; then
	require_command $MZ
fi
if [[ "$REQUIRE_TEAMD" = "yes" ]]; then
	require_command $TEAMD
fi
if [[ "$REQUIRE_MTOOLS" = "yes" ]]; then
	# https://github.com/troglobit/mtools
	require_command msend
	require_command mreceive
	check_mtools_version
fi

##############################################################################
# Command line options handling

check_env() {
	if [[ ! (( -n "$LOCAL_V4" && -n "$REMOTE_V4") ||
		 ( -n "$LOCAL_V6" && -n "$REMOTE_V6" )) ]]; then
		echo "SKIP: Invalid environment, missing or inconsistent LOCAL_V4/REMOTE_V4/LOCAL_V6/REMOTE_V6"
		echo "Please see tools/testing/selftests/drivers/net/README.rst"
		exit "$ksft_skip"
	fi

	if [[ -z "$REMOTE_TYPE" ]]; then
		echo "SKIP: Invalid environment, missing REMOTE_TYPE"
		exit "$ksft_skip"
	fi

	if [[ -z "$REMOTE_ARGS" ]]; then
		echo "SKIP: Invalid environment, missing REMOTE_ARGS"
		exit "$ksft_skip"
	fi
}

__run_on()
{
	local target=$1; shift
	local type args

	IFS=':' read -r type args <<< "$target"

	case "$type" in
	netns)
		# Execute command in network namespace
		# args contains the namespace name
		ip netns exec "$args" "$@"
		;;
	ssh)
		# Execute command via SSH args contains user@host
		ssh -n "$args" "$@"
		;;
	local|*)
		# Execute command locally. This is also the fallback
		# case for when the interface's target is not found in
		# the TARGETS array.
		"$@"
		;;
	esac
}

run_on()
{
	local iface=$1; shift
	local target="local:"

	if [ "${DRIVER_TEST_CONFORMANT}" = "yes" ]; then
		target="${TARGETS[$iface]}"
	fi

	__run_on "$target" "$@"
}

get_ifname_by_ip()
{
	local target=$1; shift
	local ip_addr=$1; shift

	__run_on "$target" ip -j addr show to "$ip_addr" | jq -r '.[].ifname'
}

# Whether the test is conforming to the requirements and usage described in
# drivers/net/README.rst.
: "${DRIVER_TEST_CONFORMANT:=no}"

declare -A TARGETS

# Based on DRIVER_TEST_CONFORMANT, decide if to source drivers/net/net.config
# or not. In the "yes" case, the test expects to pass the arguments through the
# variables specified in drivers/net/README.rst file. If not, fallback on
# parsing the script arguments for interface names.
if [ "${DRIVER_TEST_CONFORMANT}" = "yes" ]; then
	if [[ -f $net_forwarding_dir/../../drivers/net/net.config ]]; then
		source "$net_forwarding_dir/../../drivers/net/net.config"
	fi

	if (( NUM_NETIFS > 2)); then
		echo "SKIP: DRIVER_TEST_CONFORMANT=yes and NUM_NETIFS is bigger than 2"
		exit "$ksft_skip"
	fi

	check_env

	# Populate the NETIFS and TARGETS arrays automatically based on the
	# environment variables. The TARGETS array is indexed by the network
	# interface name keeping track of the target on which the interface
	# resides. Values will be strings of the following format -
	# <type>:<args>.
	#
	# TARGETS[eth0]="local:" - meaning that the eth0 interface is
	# accessible locally
	# TARGETS[eth1]="netns:foo" - eth1 is in the foo netns
	# TARGETS[eth2]="ssh:root@10.0.0.2" - eth2 is accessible through
	# running the 'ssh root@10.0.0.2' command.

	unset NETIFS
	declare -A NETIFS

	NETIFS[p1]="$NETIF"
	TARGETS[$NETIF]="local:"

	# Locate the name of the remote interface
	remote_target="$REMOTE_TYPE:$REMOTE_ARGS"
	if [[ -v REMOTE_V4 ]]; then
		remote_netif=$(get_ifname_by_ip "$remote_target" "$REMOTE_V4")
	else
		remote_netif=$(get_ifname_by_ip "$remote_target" "$REMOTE_V6")
	fi
	if [[ ! -n "$remote_netif" ]]; then
		echo "SKIP: cannot find remote interface"
		exit "$ksft_skip"
	fi

	if [[ "$NETIF" == "$remote_netif" ]]; then
		echo "SKIP: local and remote interfaces cannot have the same name"
		exit "$ksft_skip"
	fi

	NETIFS[p2]="$remote_netif"
	TARGETS[$remote_netif]="$REMOTE_TYPE:$REMOTE_ARGS"
else
	count=0
	# Prime NETIFS from the command line, but retain if none given.
	if [[ $# -gt 0 ]]; then
		unset NETIFS
		declare -A NETIFS

		while [[ $# -gt 0 ]]; do
			count=$((count + 1))
			NETIFS[p$count]="$1"
			TARGETS[$1]="local:"
			shift
		done
	fi
fi

##############################################################################
# Network interfaces configuration

if [[ ! -v NUM_NETIFS ]]; then
	echo "SKIP: importer does not define \"NUM_NETIFS\""
	exit $ksft_skip
fi

if (( NUM_NETIFS > ${#NETIFS[@]} )); then
	echo "SKIP: Importer requires $NUM_NETIFS NETIFS, but only ${#NETIFS[@]} are defined (${NETIFS[@]})"
	exit $ksft_skip
fi

for i in $(seq ${#NETIFS[@]}); do
	if [[ ! ${NETIFS[p$i]} ]]; then
		echo "SKIP: NETIFS[p$i] not given"
		exit $ksft_skip
	fi
done

create_netif_veth()
{
	local i

	for ((i = 1; i <= NUM_NETIFS; ++i)); do
		local j=$((i+1))

		if [ -z ${NETIFS[p$i]} ]; then
			echo "SKIP: Cannot create interface. Name not specified"
			exit $ksft_skip
		fi

		ip link show dev ${NETIFS[p$i]} &> /dev/null
		if [[ $? -ne 0 ]]; then
			ip link add ${NETIFS[p$i]} type veth \
				peer name ${NETIFS[p$j]}
			if [[ $? -ne 0 ]]; then
				echo "Failed to create netif"
				exit 1
			fi
		fi
		i=$j
	done
}

create_netif()
{
	case "$NETIF_TYPE" in
	veth) create_netif_veth
	      ;;
	*) echo "Can not create interfaces of type \'$NETIF_TYPE\'"
	   exit 1
	   ;;
	esac
}

declare -A MAC_ADDR_ORIG
mac_addr_prepare()
{
	local new_addr=
	local dev=

	for ((i = 1; i <= NUM_NETIFS; ++i)); do
		dev=${NETIFS[p$i]}
		new_addr=$(printf "00:01:02:03:04:%02x" $i)

		MAC_ADDR_ORIG["$dev"]=$(run_on "$dev" \
			ip -j link show dev "$dev" | jq -e '.[].address')
		# Strip quotes
		MAC_ADDR_ORIG["$dev"]=${MAC_ADDR_ORIG["$dev"]//\"/}
		run_on "$dev" ip link set dev "$dev" address $new_addr
	done
}

mac_addr_restore()
{
	local dev=

	for ((i = 1; i <= NUM_NETIFS; ++i)); do
		dev=${NETIFS[p$i]}
		run_on "$dev" \
			ip link set dev "$dev" address ${MAC_ADDR_ORIG["$dev"]}
	done
}

if [[ "$NETIF_CREATE" = "yes" ]]; then
	create_netif
fi

if [[ "$STABLE_MAC_ADDRS" = "yes" ]]; then
	mac_addr_prepare
fi

for ((i = 1; i <= NUM_NETIFS; ++i)); do
	int="${NETIFS[p$i]}"

	run_on "$int" ip link show dev "$int" &> /dev/null
	if [[ $? -ne 0 ]]; then
		echo "SKIP: could not find all required interfaces"
		exit $ksft_skip
	fi
done

##############################################################################
# Helpers

not()
{
	"$@"
	[[ $? != 0 ]]
}

get_max()
{
	local arr=("$@")

	max=${arr[0]}
	for cur in ${arr[@]}; do
		if [[ $cur -gt $max ]]; then
			max=$cur
		fi
	done

	echo $max
}

grep_bridge_fdb()
{
	local addr=$1; shift
	local word
	local flag

	if [ "$1" == "self" ] || [ "$1" == "master" ]; then
		word=$1; shift
		if [ "$1" == "-v" ]; then
			flag=$1; shift
		fi
	fi

	$@ | grep $addr | grep $flag "$word"
}

wait_for_port_up()
{
	"$@" | grep -q "Link detected: yes"
}

wait_for_offload()
{
	"$@" | grep -q offload
}

wait_for_trap()
{
	"$@" | grep -q trap
}

setup_wait_dev()
{
	local dev=$1; shift
	local wait_time=${1:-$WAIT_TIME}; shift

	setup_wait_dev_with_timeout "$dev" $INTERFACE_TIMEOUT $wait_time

	if (($?)); then
		check_err 1
		log_test setup_wait_dev ": Interface $dev does not come up."
		exit 1
	fi
}

setup_wait_dev_with_timeout()
{
	local dev=$1; shift
	local max_iterations=${1:-$WAIT_TIMEOUT}; shift
	local wait_time=${1:-$WAIT_TIME}; shift
	local i

	for ((i = 1; i <= $max_iterations; ++i)); do
		run_on "$dev" ip link show dev "$dev" up \
			| grep 'state UP' &> /dev/null
		if [[ $? -ne 0 ]]; then
			sleep 1
		else
			sleep $wait_time
			return 0
		fi
	done

	return 1
}

setup_wait_n()
{
	local num_netifs=$1; shift
	local i

	for ((i = 1; i <= num_netifs; ++i)); do
		setup_wait_dev ${NETIFS[p$i]} 0
	done

	# Make sure links are ready.
	sleep $WAIT_TIME
}

setup_wait()
{
	setup_wait_n "$NUM_NETIFS"
}

wait_for_dev()
{
        local dev=$1; shift
        local timeout=${1:-$WAIT_TIMEOUT}; shift

        slowwait $timeout ip link show dev $dev &> /dev/null
        if (( $? )); then
                check_err 1
                log_test wait_for_dev "Interface $dev did not appear."
                exit $EXIT_STATUS
        fi
}

pre_cleanup()
{
	if [ "${PAUSE_ON_CLEANUP}" = "yes" ]; then
		echo "Pausing before cleanup, hit any key to continue"
		read
	fi

	if [[ "$STABLE_MAC_ADDRS" = "yes" ]]; then
		mac_addr_restore
	fi
}

vrf_prepare()
{
	ip -4 rule add pref 32765 table local
	ip -4 rule del pref 0
	ip -6 rule add pref 32765 table local
	ip -6 rule del pref 0
}

vrf_cleanup()
{
	ip -6 rule add pref 0 table local
	ip -6 rule del pref 32765
	ip -4 rule add pref 0 table local
	ip -4 rule del pref 32765
}

adf_vrf_prepare()
{
	vrf_prepare
	defer vrf_cleanup
}

__last_tb_id=0
declare -A __TB_IDS

__vrf_td_id_assign()
{
	local vrf_name=$1

	__last_tb_id=$((__last_tb_id + 1))
	__TB_IDS[$vrf_name]=$__last_tb_id
	return $__last_tb_id
}

__vrf_td_id_lookup()
{
	local vrf_name=$1

	return ${__TB_IDS[$vrf_name]}
}

vrf_create()
{
	local vrf_name=$1
	local tb_id

	__vrf_td_id_assign $vrf_name
	tb_id=$?

	ip link add dev $vrf_name type vrf table $tb_id
	ip -4 route add table $tb_id unreachable default metric 4278198272
	ip -6 route add table $tb_id unreachable default metric 4278198272
}

vrf_destroy()
{
	local vrf_name=$1
	local tb_id

	__vrf_td_id_lookup $vrf_name
	tb_id=$?

	ip -6 route del table $tb_id unreachable default metric 4278198272
	ip -4 route del table $tb_id unreachable default metric 4278198272
	ip link del dev $vrf_name
}

__addr_add_del()
{
	local if_name=$1
	local add_del=$2
	local array

	shift
	shift
	array=("${@}")

	for addrstr in "${array[@]}"; do
		ip address $add_del $addrstr dev $if_name
	done
}

__simple_if_init()
{
	local if_name=$1; shift
	local vrf_name=$1; shift
	local addrs=("${@}")

	ip link set dev $if_name master $vrf_name
	ip link set dev $if_name up

	__addr_add_del $if_name add "${addrs[@]}"
}

__simple_if_fini()
{
	local if_name=$1; shift
	local addrs=("${@}")

	__addr_add_del $if_name del "${addrs[@]}"

	ip link set dev $if_name down
	ip link set dev $if_name nomaster
}

simple_if_init()
{
	local if_name=$1
	local vrf_name
	local array

	shift
	vrf_name=v$if_name
	array=("${@}")

	vrf_create $vrf_name
	ip link set dev $vrf_name up
	__simple_if_init $if_name $vrf_name "${array[@]}"
}

simple_if_fini()
{
	local if_name=$1
	local vrf_name
	local array

	shift
	vrf_name=v$if_name
	array=("${@}")

	__simple_if_fini $if_name "${array[@]}"
	vrf_destroy $vrf_name
}

adf_simple_if_init()
{
	simple_if_init "$@"
	defer simple_if_fini "$@"
}

tunnel_create()
{
	local name=$1; shift
	local type=$1; shift
	local local=$1; shift
	local remote=$1; shift

	ip link add name $name type $type \
	   local $local remote $remote "$@"
	ip link set dev $name up
}

tunnel_destroy()
{
	local name=$1; shift

	ip link del dev $name
}

vlan_create()
{
	local if_name=$1; shift
	local vid=$1; shift
	local vrf=$1; shift
	local ips=("${@}")
	local name=$if_name.$vid

	ip link add name $name link $if_name type vlan id $vid
	if [ "$vrf" != "" ]; then
		ip link set dev $name master $vrf
	fi
	ip link set dev $name up
	__addr_add_del $name add "${ips[@]}"
}

vlan_destroy()
{
	local if_name=$1; shift
	local vid=$1; shift
	local name=$if_name.$vid

	ip link del dev $name
}

team_create()
{
	local if_name=$1; shift
	local mode=$1; shift

	require_command $TEAMD
	$TEAMD -t $if_name -d -c '{"runner": {"name": "'$mode'"}}'
	for slave in "$@"; do
		ip link set dev $slave down
		ip link set dev $slave master $if_name
		ip link set dev $slave up
	done
	ip link set dev $if_name up
}

team_destroy()
{
	local if_name=$1; shift

	$TEAMD -t $if_name -k
}

master_name_get()
{
	local if_name=$1

	ip -j link show dev $if_name | jq -r '.[]["master"]'
}

link_stats_get()
{
	local if_name=$1; shift
	local dir=$1; shift
	local stat=$1; shift

	ip -j -s link show dev $if_name \
		| jq '.[]["stats64"]["'$dir'"]["'$stat'"]'
}

link_stats_tx_packets_get()
{
	link_stats_get $1 tx packets
}

link_stats_rx_errors_get()
{
	link_stats_get $1 rx errors
}

ethtool_stats_get()
{
	local dev=$1; shift
	local stat=$1; shift

	ethtool -S $dev | grep "^ *$stat:" | head -n 1 | cut -d: -f2
}

ethtool_std_stats_get()
{
	local dev=$1; shift
	local grp=$1; shift
	local name=$1; shift
	local src=$1; shift

	if [[ "$grp" == "pause" ]]; then
		run_on "$dev" ethtool -I --json -a "$dev" --src "$src" | \
			jq --arg name "$name" '.[].statistics[$name]'
		return
	fi

	run_on "$dev" \
		ethtool --json -S "$dev" --groups "$grp" -- --src "$src" | \
		jq --arg grp "$grp" --arg name "$name" '.[][$grp][$name]'
}

qdisc_stats_get()
{
	local dev=$1; shift
	local handle=$1; shift
	local selector=$1; shift

	tc -j -s qdisc show dev "$dev" \
	    | jq '.[] | select(.handle == "'"$handle"'") | '"$selector"
}

qdisc_parent_stats_get()
{
	local dev=$1; shift
	local parent=$1; shift
	local selector=$1; shift

	tc -j -s qdisc show dev "$dev" invisible \
	    | jq '.[] | select(.parent == "'"$parent"'") | '"$selector"
}

ipv6_stats_get()
{
	local dev=$1; shift
	local stat=$1; shift

	cat /proc/net/dev_snmp6/$dev | grep "^$stat" | cut -f2
}

hw_stats_get()
{
	local suite=$1; shift
	local if_name=$1; shift
	local dir=$1; shift
	local stat=$1; shift

	ip -j stats show dev $if_name group offload subgroup $suite |
		jq ".[0].stats64.$dir.$stat"
}

__nh_stats_get()
{
	local key=$1; shift
	local group_id=$1; shift
	local member_id=$1; shift

	ip -j -s -s nexthop show id $group_id |
	    jq --argjson member_id "$member_id" --arg key "$key" \
	       '.[].group_stats[] | select(.id == $member_id) | .[$key]'
}

nh_stats_get()
{
	local group_id=$1; shift
	local member_id=$1; shift

	__nh_stats_get packets "$group_id" "$member_id"
}

nh_stats_get_hw()
{
	local group_id=$1; shift
	local member_id=$1; shift

	__nh_stats_get packets_hw "$group_id" "$member_id"
}

humanize()
{
	local speed=$1; shift

	for unit in bps Kbps Mbps Gbps; do
		if (($(echo "$speed < 1024" | bc))); then
			break
		fi

		speed=$(echo "scale=1; $speed / 1024" | bc)
	done

	echo "$speed${unit}"
}

rate()
{
	local t0=$1; shift
	local t1=$1; shift
	local interval=$1; shift

	echo $((8 * (t1 - t0) / interval))
}

packets_rate()
{
	local t0=$1; shift
	local t1=$1; shift
	local interval=$1; shift

	echo $(((t1 - t0) / interval))
}

ether_addr_to_u64()
{
	local addr="$1"
	local order="$((1 << 40))"
	local val=0
	local byte

	addr="${addr//:/ }"

	for byte in $addr; do
		byte="0x$byte"
		val=$((val + order * byte))
		order=$((order >> 8))
	done

	printf "0x%x" $val
}

u64_to_ether_addr()
{
	local val=$1
	local byte
	local i

	for ((i = 40; i >= 0; i -= 8)); do
		byte=$(((val & (0xff << i)) >> i))
		printf "%02x" $byte
		if [ $i -ne 0 ]; then
			printf ":"
		fi
	done
}

ipv6_lladdr_get()
{
	local if_name=$1

	ip -j addr show dev $if_name | \
		jq -r '.[]["addr_info"][] | select(.scope == "link").local' | \
		head -1
}

bridge_ageing_time_get()
{
	local bridge=$1
	local ageing_time

	# Need to divide by 100 to convert to seconds.
	ageing_time=$(ip -j -d link show dev $bridge \
		      | jq '.[]["linkinfo"]["info_data"]["ageing_time"]')
	echo $((ageing_time / 100))
}

declare -A SYSCTL_ORIG
sysctl_save()
{
	local key=$1; shift

	SYSCTL_ORIG[$key]=$(sysctl -n $key)
}

sysctl_set()
{
	local key=$1; shift
	local value=$1; shift

	sysctl_save "$key"
	sysctl -qw $key="$value"
}

sysctl_restore()
{
	local key=$1; shift

	sysctl -qw $key="${SYSCTL_ORIG[$key]}"
}

forwarding_enable()
{
	sysctl_set net.ipv4.conf.all.forwarding 1
	sysctl_set net.ipv6.conf.all.forwarding 1
}

forwarding_restore()
{
	sysctl_restore net.ipv6.conf.all.forwarding
	sysctl_restore net.ipv4.conf.all.forwarding
}

adf_forwarding_enable()
{
	forwarding_enable
	defer forwarding_restore
}

declare -A MTU_ORIG
mtu_set()
{
	local dev=$1; shift
	local mtu=$1; shift

	MTU_ORIG["$dev"]=$(ip -j link show dev $dev | jq -e '.[].mtu')
	ip link set dev $dev mtu $mtu
}

mtu_restore()
{
	local dev=$1; shift

	ip link set dev $dev mtu ${MTU_ORIG["$dev"]}
}

tc_offload_check()
{
	local num_netifs=${1:-$NUM_NETIFS}

	for ((i = 1; i <= num_netifs; ++i)); do
		ethtool -k ${NETIFS[p$i]} \
			| grep "hw-tc-offload: on" &> /dev/null
		if [[ $? -ne 0 ]]; then
			return 1
		fi
	done

	return 0
}

trap_install()
{
	local dev=$1; shift
	local direction=$1; shift

	# Some devices may not support or need in-hardware trapping of traffic
	# (e.g. the veth pairs that this library creates for non-existent
	# loopbacks). Use continue instead, so that there is a filter in there
	# (some tests check counters), and so that other filters are still
	# processed.
	tc filter add dev $dev $direction pref 1 \
		flower skip_sw action trap 2>/dev/null \
	    || tc filter add dev $dev $direction pref 1 \
		       flower action continue
}

trap_uninstall()
{
	local dev=$1; shift
	local direction=$1; shift

	tc filter del dev $dev $direction pref 1 flower
}

__icmp_capture_add_del()
{
	local add_del=$1; shift
	local pref=$1; shift
	local vsuf=$1; shift
	local tundev=$1; shift
	local filter=$1; shift

	tc filter $add_del dev "$tundev" ingress \
	   proto ip$vsuf pref $pref \
	   flower ip_proto icmp$vsuf $filter \
	   action pass
}

icmp_capture_install()
{
	local tundev=$1; shift
	local filter=$1; shift

	__icmp_capture_add_del add 100 "" "$tundev" "$filter"
}

icmp_capture_uninstall()
{
	local tundev=$1; shift
	local filter=$1; shift

	__icmp_capture_add_del del 100 "" "$tundev" "$filter"
}

icmp6_capture_install()
{
	local tundev=$1; shift
	local filter=$1; shift

	__icmp_capture_add_del add 100 v6 "$tundev" "$filter"
}

icmp6_capture_uninstall()
{
	local tundev=$1; shift
	local filter=$1; shift

	__icmp_capture_add_del del 100 v6 "$tundev" "$filter"
}

__vlan_capture_add_del()
{
	local add_del=$1; shift
	local pref=$1; shift
	local dev=$1; shift
	local filter=$1; shift

	tc filter $add_del dev "$dev" ingress \
	   proto 802.1q pref $pref \
	   flower $filter \
	   action pass
}

vlan_capture_install()
{
	local dev=$1; shift
	local filter=$1; shift

	__vlan_capture_add_del add 100 "$dev" "$filter"
}

vlan_capture_uninstall()
{
	local dev=$1; shift
	local filter=$1; shift

	__vlan_capture_add_del del 100 "$dev" "$filter"
}

__dscp_capture_add_del()
{
	local add_del=$1; shift
	local dev=$1; shift
	local base=$1; shift
	local dscp;

	for prio in {0..7}; do
		dscp=$((base + prio))
		__icmp_capture_add_del $add_del $((dscp + 100)) "" $dev \
				       "skip_hw ip_tos $((dscp << 2))"
	done
}

dscp_capture_install()
{
	local dev=$1; shift
	local base=$1; shift

	__dscp_capture_add_del add $dev $base
}

dscp_capture_uninstall()
{
	local dev=$1; shift
	local base=$1; shift

	__dscp_capture_add_del del $dev $base
}

dscp_fetch_stats()
{
	local dev=$1; shift
	local base=$1; shift

	for prio in {0..7}; do
		local dscp=$((base + prio))
		local t=$(tc_rule_stats_get $dev $((dscp + 100)))
		echo "[$dscp]=$t "
	done
}

matchall_sink_create()
{
	local dev=$1; shift

	tc qdisc add dev $dev clsact
	tc filter add dev $dev ingress \
	   pref 10000 \
	   matchall \
	   action drop
}

cleanup()
{
	pre_cleanup
	defer_scopes_cleanup
}

multipath_eval()
{
	local desc="$1"
	local weight_rp12=$2
	local weight_rp13=$3
	local packets_rp12=$4
	local packets_rp13=$5
	local weights_ratio packets_ratio diff

	RET=0

	if [[ "$weight_rp12" -gt "$weight_rp13" ]]; then
		weights_ratio=$(echo "scale=2; $weight_rp12 / $weight_rp13" \
				| bc -l)
	else
		weights_ratio=$(echo "scale=2; $weight_rp13 / $weight_rp12" \
				| bc -l)
	fi

	if [[ "$packets_rp12" -eq "0" || "$packets_rp13" -eq "0" ]]; then
	       check_err 1 "Packet difference is 0"
	       log_test "Multipath"
	       log_info "Expected ratio $weights_ratio"
	       return
	fi

	if [[ "$weight_rp12" -gt "$weight_rp13" ]]; then
		packets_ratio=$(echo "scale=2; $packets_rp12 / $packets_rp13" \
				| bc -l)
	else
		packets_ratio=$(echo "scale=2; $packets_rp13 / $packets_rp12" \
				| bc -l)
	fi

	diff=$(echo $weights_ratio - $packets_ratio | bc -l)
	diff=${diff#-}

	test "$(echo "$diff / $weights_ratio > 0.15" | bc -l)" -eq 0
	check_err $? "Too large discrepancy between expected and measured ratios"
	log_test "$desc"
	log_info "Expected ratio $weights_ratio Measured ratio $packets_ratio"
}

in_ns()
{
	local name=$1; shift

	ip netns exec $name bash <<-EOF
		NUM_NETIFS=0
		source lib.sh
		$(for a in "$@"; do printf "%q${IFS:0:1}" "$a"; done)
	EOF
}

##############################################################################
# Tests

ping_do()
{
	local if_name=$1
	local dip=$2
	local args=$3
	local vrf_name

	vrf_name=$(master_name_get $if_name)
	ip vrf exec $vrf_name \
		$PING $args -c $PING_COUNT -i 0.1 \
		-w $PING_TIMEOUT $dip &> /dev/null
}

ping_test()
{
	RET=0

	ping_do $1 $2
	check_err $?
	log_test "ping$3"
}

ping_test_fails()
{
	RET=0

	ping_do $1 $2
	check_fail $?
	log_test "ping fails$3"
}

ping6_do()
{
	local if_name=$1
	local dip=$2
	local args=$3
	local vrf_name

	vrf_name=$(master_name_get $if_name)
	ip vrf exec $vrf_name \
		$PING6 $args -c $PING_COUNT -i 0.1 \
		-w $PING_TIMEOUT $dip &> /dev/null
}

ping6_test()
{
	RET=0

	ping6_do $1 $2
	check_err $?
	log_test "ping6$3"
}

ping6_test_fails()
{
	RET=0

	ping6_do $1 $2
	check_fail $?
	log_test "ping6 fails$3"
}

learning_test()
{
	local bridge=$1
	local br_port1=$2	# Connected to `host1_if`.
	local host1_if=$3
	local host2_if=$4
	local mac=de:ad:be:ef:13:37
	local ageing_time

	RET=0

	bridge -j fdb show br $bridge brport $br_port1 \
		| jq -e ".[] | select(.mac == \"$mac\")" &> /dev/null
	check_fail $? "Found FDB record when should not"

	# Disable unknown unicast flooding on `br_port1` to make sure
	# packets are only forwarded through the port after a matching
	# FDB entry was installed.
	bridge link set dev $br_port1 flood off

	ip link set $host1_if promisc on
	tc qdisc add dev $host1_if ingress
	tc filter add dev $host1_if ingress protocol ip pref 1 handle 101 \
		flower dst_mac $mac action drop

	$MZ $host2_if -c 1 -p 64 -b $mac -t ip -q
	sleep 1

	tc -j -s filter show dev $host1_if ingress \
		| jq -e ".[] | select(.options.handle == 101) \
		| select(.options.actions[0].stats.packets == 1)" &> /dev/null
	check_fail $? "Packet reached first host when should not"

	$MZ $host1_if -c 1 -p 64 -a $mac -t ip -q
	sleep 1

	bridge -j fdb show br $bridge brport $br_port1 \
		| jq -e ".[] | select(.mac == \"$mac\")" &> /dev/null
	check_err $? "Did not find FDB record when should"

	$MZ $host2_if -c 1 -p 64 -b $mac -t ip -q
	sleep 1

	tc -j -s filter show dev $host1_if ingress \
		| jq -e ".[] | select(.options.handle == 101) \
		| select(.options.actions[0].stats.packets == 1)" &> /dev/null
	check_err $? "Packet did not reach second host when should"

	# Wait for 10 seconds after the ageing time to make sure FDB
	# record was aged-out.
	ageing_time=$(bridge_ageing_time_get $bridge)
	sleep $((ageing_time + 10))

	bridge -j fdb show br $bridge brport $br_port1 \
		| jq -e ".[] | select(.mac == \"$mac\")" &> /dev/null
	check_fail $? "Found FDB record when should not"

	bridge link set dev $br_port1 learning off

	$MZ $host1_if -c 1 -p 64 -a $mac -t ip -q
	sleep 1

	bridge -j fdb show br $bridge brport $br_port1 \
		| jq -e ".[] | select(.mac == \"$mac\")" &> /dev/null
	check_fail $? "Found FDB record when should not"

	bridge link set dev $br_port1 learning on

	tc filter del dev $host1_if ingress protocol ip pref 1 handle 101 flower
	tc qdisc del dev $host1_if ingress
	ip link set $host1_if promisc off

	bridge link set dev $br_port1 flood on

	log_test "FDB learning"
}

flood_test_do()
{
	local should_flood=$1
	local mac=$2
	local ip=$3
	local host1_if=$4
	local host2_if=$5
	local err=0

	# Add an ACL on `host2_if` which will tell us whether the packet
	# was flooded to it or not.
	ip link set $host2_if promisc on
	tc qdisc add dev $host2_if ingress
	tc filter add dev $host2_if ingress protocol ip pref 1 handle 101 \
		flower dst_mac $mac action drop

	$MZ $host1_if -c 1 -p 64 -b $mac -B $ip -t ip -q
	sleep 1

	tc -j -s filter show dev $host2_if ingress \
		| jq -e ".[] | select(.options.handle == 101) \
		| select(.options.actions[0].stats.packets == 1)" &> /dev/null
	if [[ $? -ne 0 && $should_flood == "true" || \
	      $? -eq 0 && $should_flood == "false" ]]; then
		err=1
	fi

	tc filter del dev $host2_if ingress protocol ip pref 1 handle 101 flower
	tc qdisc del dev $host2_if ingress
	ip link set $host2_if promisc off

	return $err
}

flood_unicast_test()
{
	local br_port=$1
	local host1_if=$2
	local host2_if=$3
	local mac=de:ad:be:ef:13:37
	local ip=192.0.2.100

	RET=0

	bridge link set dev $br_port flood off

	flood_test_do false $mac $ip $host1_if $host2_if
	check_err $? "Packet flooded when should not"

	bridge link set dev $br_port flood on

	flood_test_do true $mac $ip $host1_if $host2_if
	check_err $? "Packet was not flooded when should"

	log_test "Unknown unicast flood"
}

flood_multicast_test()
{
	local br_port=$1
	local host1_if=$2
	local host2_if=$3
	local mac=01:00:5e:00:00:01
	local ip=239.0.0.1

	RET=0

	bridge link set dev $br_port mcast_flood off

	flood_test_do false $mac $ip $host1_if $host2_if
	check_err $? "Packet flooded when should not"

	bridge link set dev $br_port mcast_flood on

	flood_test_do true $mac $ip $host1_if $host2_if
	check_err $? "Packet was not flooded when should"

	log_test "Unregistered multicast flood"
}

flood_test()
{
	# `br_port` is connected to `host2_if`
	local br_port=$1
	local host1_if=$2
	local host2_if=$3

	flood_unicast_test $br_port $host1_if $host2_if
	flood_multicast_test $br_port $host1_if $host2_if
}

__start_traffic()
{
	local pktsize=$1; shift
	local proto=$1; shift
	local h_in=$1; shift    # Where the traffic egresses the host
	local sip=$1; shift
	local dip=$1; shift
	local dmac=$1; shift
	local -a mz_args=("$@")

	$MZ $h_in -p $pktsize -A $sip -B $dip -c 0 \
		-a own -b $dmac -t "$proto" -q "${mz_args[@]}" &
	sleep 1
}

start_traffic_pktsize()
{
	local pktsize=$1; shift
	local h_in=$1; shift
	local sip=$1; shift
	local dip=$1; shift
	local dmac=$1; shift
	local -a mz_args=("$@")

	__start_traffic $pktsize udp "$h_in" "$sip" "$dip" "$dmac" \
			"${mz_args[@]}"
}

start_tcp_traffic_pktsize()
{
	local pktsize=$1; shift
	local h_in=$1; shift
	local sip=$1; shift
	local dip=$1; shift
	local dmac=$1; shift
	local -a mz_args=("$@")

	__start_traffic $pktsize tcp "$h_in" "$sip" "$dip" "$dmac" \
			"${mz_args[@]}"
}

start_traffic()
{
	local h_in=$1; shift
	local sip=$1; shift
	local dip=$1; shift
	local dmac=$1; shift
	local -a mz_args=("$@")

	start_traffic_pktsize 8000 "$h_in" "$sip" "$dip" "$dmac" \
			      "${mz_args[@]}"
}

start_tcp_traffic()
{
	local h_in=$1; shift
	local sip=$1; shift
	local dip=$1; shift
	local dmac=$1; shift
	local -a mz_args=("$@")

	start_tcp_traffic_pktsize 8000 "$h_in" "$sip" "$dip" "$dmac" \
				  "${mz_args[@]}"
}

stop_traffic()
{
	local pid=${1-%%}; shift

	kill_process "$pid"
}

declare -A cappid
declare -A capfile
declare -A capout

tcpdump_start()
{
	local if_name=$1; shift
	local ns=$1; shift

	capfile[$if_name]=$(mktemp)
	capout[$if_name]=$(mktemp)

	if [ -z $ns ]; then
		ns_cmd=""
	else
		ns_cmd="ip netns exec ${ns}"
	fi

	if [ -z $SUDO_USER ] ; then
		capuser=""
	else
		capuser="-Z $SUDO_USER"
	fi

	$ns_cmd tcpdump $TCPDUMP_EXTRA_FLAGS -e -n -Q in -i $if_name \
		-s 65535 -B 32768 $capuser -w ${capfile[$if_name]} \
		> "${capout[$if_name]}" 2>&1 &
	cappid[$if_name]=$!

	sleep 1
}

tcpdump_stop_nosleep()
{
	local if_name=$1
	local pid=${cappid[$if_name]}

	$ns_cmd kill "$pid" && wait "$pid"
}

tcpdump_stop()
{
	tcpdump_stop_nosleep "$1"
	sleep 1
}

tcpdump_cleanup()
{
	local if_name=$1

	rm ${capfile[$if_name]} ${capout[$if_name]}
}

tcpdump_show()
{
	local if_name=$1

	tcpdump -e -nn -r ${capfile[$if_name]} 2>&1
}

# return 0 if the packet wasn't seen on host2_if or 1 if it was
mcast_packet_test()
{
	local mac=$1
	local src_ip=$2
	local ip=$3
	local host1_if=$4
	local host2_if=$5
	local seen=0
	local tc_proto="ip"
	local mz_v6arg=""

	# basic check to see if we were passed an IPv4 address, if not assume IPv6
	if [[ ! $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
		tc_proto="ipv6"
		mz_v6arg="-6"
	fi

	# Add an ACL on `host2_if` which will tell us whether the packet
	# was received by it or not.
	tc qdisc add dev $host2_if ingress
	tc filter add dev $host2_if ingress protocol $tc_proto pref 1 handle 101 \
		flower ip_proto udp dst_mac $mac action drop

	$MZ $host1_if $mz_v6arg -c 1 -p 64 -b $mac -A $src_ip -B $ip -t udp "dp=4096,sp=2048" -q
	sleep 1

	tc -j -s filter show dev $host2_if ingress \
		| jq -e ".[] | select(.options.handle == 101) \
		| select(.options.actions[0].stats.packets == 1)" &> /dev/null
	if [[ $? -eq 0 ]]; then
		seen=1
	fi

	tc filter del dev $host2_if ingress protocol $tc_proto pref 1 handle 101 flower
	tc qdisc del dev $host2_if ingress

	return $seen
}

brmcast_check_sg_entries()
{
	local report=$1; shift
	local slist=("$@")
	local sarg=""

	for src in "${slist[@]}"; do
		sarg="${sarg} and .source_list[].address == \"$src\""
	done
	bridge -j -d -s mdb show dev br0 \
		| jq -e ".[].mdb[] | \
			 select(.grp == \"$TEST_GROUP\" and .source_list != null $sarg)" &>/dev/null
	check_err $? "Wrong *,G entry source list after $report report"

	for sgent in "${slist[@]}"; do
		bridge -j -d -s mdb show dev br0 \
			| jq -e ".[].mdb[] | \
				 select(.grp == \"$TEST_GROUP\" and .src == \"$sgent\")" &>/dev/null
		check_err $? "Missing S,G entry ($sgent, $TEST_GROUP)"
	done
}

brmcast_check_sg_fwding()
{
	local should_fwd=$1; shift
	local sources=("$@")

	for src in "${sources[@]}"; do
		local retval=0

		mcast_packet_test $TEST_GROUP_MAC $src $TEST_GROUP $h2 $h1
		retval=$?
		if [ $should_fwd -eq 1 ]; then
			check_fail $retval "Didn't forward traffic from S,G ($src, $TEST_GROUP)"
		else
			check_err $retval "Forwarded traffic for blocked S,G ($src, $TEST_GROUP)"
		fi
	done
}

brmcast_check_sg_state()
{
	local is_blocked=$1; shift
	local sources=("$@")
	local should_fail=1

	if [ $is_blocked -eq 1 ]; then
		should_fail=0
	fi

	for src in "${sources[@]}"; do
		bridge -j -d -s mdb show dev br0 \
			| jq -e ".[].mdb[] | \
				 select(.grp == \"$TEST_GROUP\" and .source_list != null) |
				 .source_list[] |
				 select(.address == \"$src\") |
				 select(.timer == \"0.00\")" &>/dev/null
		check_err_fail $should_fail $? "Entry $src has zero timer"

		bridge -j -d -s mdb show dev br0 \
			| jq -e ".[].mdb[] | \
				 select(.grp == \"$TEST_GROUP\" and .src == \"$src\" and \
				 .flags[] == \"blocked\")" &>/dev/null
		check_err_fail $should_fail $? "Entry $src has blocked flag"
	done
}

mc_join()
{
	local if_name=$1
	local group=$2
	local vrf_name=$(master_name_get $if_name)

	# We don't care about actual reception, just about joining the
	# IP multicast group and adding the L2 address to the device's
	# MAC filtering table
	ip vrf exec $vrf_name \
		mreceive -g $group -I $if_name > /dev/null 2>&1 &
	mreceive_pid=$!

	sleep 1
}

mc_leave()
{
	kill "$mreceive_pid" && wait "$mreceive_pid"
}

mc_send()
{
	local if_name=$1
	local groups=$2
	local vrf_name=$(master_name_get $if_name)

	ip vrf exec $vrf_name \
		msend -g $groups -I $if_name -c 1 > /dev/null 2>&1
}

adf_mcd_start()
{
	local ifs=("$@")

	local table_name="$MCD_TABLE_NAME"
	local smcroutedir
	local pid
	local if
	local i

	check_command "$MCD" || return 1
	check_command "$MC_CLI" || return 1

	smcroutedir=$(mktemp -d)
	defer rm -rf "$smcroutedir"

	for ((i = 1; i <= NUM_NETIFS; ++i)); do
		echo "phyint ${NETIFS[p$i]} enable" >> \
			"$smcroutedir/$table_name.conf"
	done

	for if in "${ifs[@]}"; do
		if ! ip_link_has_flag "$if" MULTICAST; then
			ip link set dev "$if" multicast on
			defer ip link set dev "$if" multicast off
		fi

		echo "phyint $if enable" >> \
			"$smcroutedir/$table_name.conf"
	done

	"$MCD" -N -I "$table_name" -f "$smcroutedir/$table_name.conf" \
		-P "$smcroutedir/$table_name.pid"
	busywait "$BUSYWAIT_TIMEOUT" test -e "$smcroutedir/$table_name.pid"
	pid=$(cat "$smcroutedir/$table_name.pid")
	defer kill_process "$pid"
}

mc_cli()
{
	local table_name="$MCD_TABLE_NAME"

        "$MC_CLI" -I "$table_name" "$@"
}

start_ip_monitor()
{
	local mtype=$1; shift
	local ip=${1-ip}; shift

	# start the monitor in the background
	tmpfile=`mktemp /var/run/nexthoptestXXX`
	mpid=`($ip monitor $mtype > $tmpfile & echo $!) 2>/dev/null`
	sleep 0.2
	echo "$mpid $tmpfile"
}

stop_ip_monitor()
{
	local mpid=$1; shift
	local tmpfile=$1; shift
	local el=$1; shift
	local what=$1; shift

	sleep 0.2
	kill $mpid
	local lines=`grep '^\w' $tmpfile | wc -l`
	test $lines -eq $el
	check_err $? "$what: $lines lines of events, expected $el"
	rm -rf $tmpfile
}

hw_stats_monitor_test()
{
	local dev=$1; shift
	local type=$1; shift
	local make_suitable=$1; shift
	local make_unsuitable=$1; shift
	local ip=${1-ip}; shift

	RET=0

	# Expect a notification about enablement.
	local ipmout=$(start_ip_monitor stats "$ip")
	$ip stats set dev $dev ${type}_stats on
	stop_ip_monitor $ipmout 1 "${type}_stats enablement"

	# Expect a notification about offload.
	local ipmout=$(start_ip_monitor stats "$ip")
	$make_suitable
	stop_ip_monitor $ipmout 1 "${type}_stats installation"

	# Expect a notification about loss of offload.
	local ipmout=$(start_ip_monitor stats "$ip")
	$make_unsuitable
	stop_ip_monitor $ipmout 1 "${type}_stats deinstallation"

	# Expect a notification about disablement
	local ipmout=$(start_ip_monitor stats "$ip")
	$ip stats set dev $dev ${type}_stats off
	stop_ip_monitor $ipmout 1 "${type}_stats disablement"

	log_test "${type}_stats notifications"
}

ipv4_to_bytes()
{
	local IP=$1; shift

	printf '%02x:' ${IP//./ } |
	    sed 's/:$//'
}

# Convert a given IPv6 address, `IP' such that the :: token, if present, is
# expanded, and each 16-bit group is padded with zeroes to be 4 hexadecimal
# digits. An optional `BYTESEP' parameter can be given to further separate
# individual bytes of each 16-bit group.
expand_ipv6()
{
	local IP=$1; shift
	local bytesep=$1; shift

	local cvt_ip=${IP/::/_}
	local colons=${cvt_ip//[^:]/}
	local allcol=:::::::
	# IP where :: -> the appropriate number of colons:
	local allcol_ip=${cvt_ip/_/${allcol:${#colons}}}

	echo $allcol_ip | tr : '\n' |
	    sed s/^/0000/ |
	    sed 's/.*\(..\)\(..\)/\1'"$bytesep"'\2/' |
	    tr '\n' : |
	    sed 's/:$//'
}

ipv6_to_bytes()
{
	local IP=$1; shift

	expand_ipv6 "$IP" :
}

u16_to_bytes()
{
	local u16=$1; shift

	printf "%04x" $u16 | sed 's/^/000/;s/^.*\(..\)\(..\)$/\1:\2/'
}

# Given a mausezahn-formatted payload (colon-separated bytes given as %02x),
# possibly with a keyword CHECKSUM stashed where a 16-bit checksum should be,
# calculate checksum as per RFC 1071, assuming the CHECKSUM field (if any)
# stands for 00:00.
payload_template_calc_checksum()
{
	local payload=$1; shift

	(
	    # Set input radix.
	    echo "16i"
	    # Push zero for the initial checksum.
	    echo 0

	    # Pad the payload with a terminating 00: in case we get an odd
	    # number of bytes.
	    echo "${payload%:}:00:" |
		sed 's/CHECKSUM/00:00/g' |
		tr '[:lower:]' '[:upper:]' |
		# Add the word to the checksum.
		sed 's/\(..\):\(..\):/\1\2+\n/g' |
		# Strip the extra odd byte we pushed if left unconverted.
		sed 's/\(..\):$//'

	    echo "10000 ~ +"	# Calculate and add carry.
	    echo "FFFF r - p"	# Bit-flip and print.
	) |
	    dc |
	    tr '[:upper:]' '[:lower:]'
}

payload_template_expand_checksum()
{
	local payload=$1; shift
	local checksum=$1; shift

	local ckbytes=$(u16_to_bytes $checksum)

	echo "$payload" | sed "s/CHECKSUM/$ckbytes/g"
}

payload_template_nbytes()
{
	local payload=$1; shift

	payload_template_expand_checksum "${payload%:}" 0 |
		sed 's/:/\n/g' | wc -l
}

igmpv3_is_in_get()
{
	local GRP=$1; shift
	local sources=("$@")

	local igmpv3
	local nsources=$(u16_to_bytes ${#sources[@]})

	# IS_IN ( $sources )
	igmpv3=$(:
		)"22:"$(			: Type - Membership Report
		)"00:"$(			: Reserved
		)"CHECKSUM:"$(			: Checksum
		)"00:00:"$(			: Reserved
		)"00:01:"$(			: Number of Group Records
		)"01:"$(			: Record Type - IS_IN
		)"00:"$(			: Aux Data Len
		)"${nsources}:"$(		: Number of Sources
		)"$(ipv4_to_bytes $GRP):"$(	: Multicast Address
		)"$(for src in "${sources[@]}"; do
			ipv4_to_bytes $src
			echo -n :
		    done)"$(			: Source Addresses
		)
	local checksum=$(payload_template_calc_checksum "$igmpv3")

	payload_template_expand_checksum "$igmpv3" $checksum
}

igmpv2_leave_get()
{
	local GRP=$1; shift

	local payload=$(:
		)"17:"$(			: Type - Leave Group
		)"00:"$(			: Max Resp Time - not meaningful
		)"CHECKSUM:"$(			: Checksum
		)"$(ipv4_to_bytes $GRP)"$(	: Group Address
		)
	local checksum=$(payload_template_calc_checksum "$payload")

	payload_template_expand_checksum "$payload" $checksum
}

mldv2_is_in_get()
{
	local SIP=$1; shift
	local GRP=$1; shift
	local sources=("$@")

	local hbh
	local icmpv6
	local nsources=$(u16_to_bytes ${#sources[@]})

	hbh=$(:
		)"3a:"$(			: Next Header - ICMPv6
		)"00:"$(			: Hdr Ext Len
		)"00:00:00:00:00:00:"$(		: Options and Padding
		)

	icmpv6=$(:
		)"8f:"$(			: Type - MLDv2 Report
		)"00:"$(			: Code
		)"CHECKSUM:"$(			: Checksum
		)"00:00:"$(			: Reserved
		)"00:01:"$(			: Number of Group Records
		)"01:"$(			: Record Type - IS_IN
		)"00:"$(			: Aux Data Len
		)"${nsources}:"$(		: Number of Sources
		)"$(ipv6_to_bytes $GRP):"$(	: Multicast address
		)"$(for src in "${sources[@]}"; do
			ipv6_to_bytes $src
			echo -n :
		    done)"$(			: Source Addresses
		)

	local len=$(u16_to_bytes $(payload_template_nbytes $icmpv6))
	local sudohdr=$(:
		)"$(ipv6_to_bytes $SIP):"$(	: SIP
		)"$(ipv6_to_bytes $GRP):"$(	: DIP is multicast address
	        )"${len}:"$(			: Upper-layer length
	        )"00:3a:"$(			: Zero and next-header
	        )
	local checksum=$(payload_template_calc_checksum ${sudohdr}${icmpv6})

	payload_template_expand_checksum "$hbh$icmpv6" $checksum
}

mldv1_done_get()
{
	local SIP=$1; shift
	local GRP=$1; shift

	local hbh
	local icmpv6

	hbh=$(:
		)"3a:"$(			: Next Header - ICMPv6
		)"00:"$(			: Hdr Ext Len
		)"00:00:00:00:00:00:"$(		: Options and Padding
		)

	icmpv6=$(:
		)"84:"$(			: Type - MLDv1 Done
		)"00:"$(			: Code
		)"CHECKSUM:"$(			: Checksum
		)"00:00:"$(			: Max Resp Delay - not meaningful
		)"00:00:"$(			: Reserved
		)"$(ipv6_to_bytes $GRP):"$(	: Multicast address
		)

	local len=$(u16_to_bytes $(payload_template_nbytes $icmpv6))
	local sudohdr=$(:
		)"$(ipv6_to_bytes $SIP):"$(	: SIP
		)"$(ipv6_to_bytes $GRP):"$(	: DIP is multicast address
	        )"${len}:"$(			: Upper-layer length
	        )"00:3a:"$(			: Zero and next-header
	        )
	local checksum=$(payload_template_calc_checksum ${sudohdr}${icmpv6})

	payload_template_expand_checksum "$hbh$icmpv6" $checksum
}

bail_on_lldpad()
{
	local reason1="$1"; shift
	local reason2="$1"; shift
	local caller=${FUNCNAME[1]}
	local src=${BASH_SOURCE[1]}

	if systemctl is-active --quiet lldpad; then

		cat >/dev/stderr <<-EOF
		WARNING: lldpad is running

			lldpad will likely $reason1, and this test will
			$reason2. Both are not supported at the same time,
			one of them is arbitrarily going to overwrite the
			other. That will cause spurious failures (or, unlikely,
			passes) of this test.
		EOF

		if [[ -z $ALLOW_LLDPAD ]]; then
			cat >/dev/stderr <<-EOF

				If you want to run the test anyway, please set
				an environment variable ALLOW_LLDPAD to a
				non-empty string.
			EOF
			log_test_skip $src:$caller
			exit $EXIT_STATUS
		else
			return
		fi
	fi
}

absval()
{
	local v=$1; shift

	echo $((v > 0 ? v : -v))
}

has_unicast_flt()
{
	local dev=$1; shift
	local mac_addr=$(mac_get $dev)
	local tmp=$(ether_addr_to_u64 $mac_addr)
	local promisc

	ip link set $dev up
	ip link add link $dev name macvlan-tmp type macvlan mode private
	ip link set macvlan-tmp address $(u64_to_ether_addr $((tmp + 1)))
	ip link set macvlan-tmp up

	promisc=$(ip -j -d link show dev $dev | jq -r '.[].promiscuity')

	ip link del macvlan-tmp

	[[ $promisc == 1 ]] && echo "no" || echo "yes"
}
