#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test propagation of a real device's state to the VLANs stacked on top of it
# when the real device is (or becomes) a bond member.
#
# The kernel mirrors a real device's UP/DOWN, MTU and feature changes onto its
# VLANs.  This is done asynchronously (netdev_work): doing it synchronously from
# the real device's notifier could deadlock.  If the real device is brought up
# while enslaved to a bond - so its instance lock is held across NETDEV_UP - and
# a VLAN on top of it is itself a bond member, the synchronous propagation
# re-entered the stack and tried to take the same instance lock again.
#
# Cover both halves:
#  - the deferred UP/DOWN, MTU and feature propagation actually lands on the
#    VLAN (link state and MTU use an ops-locked dummy, i.e. the deferral path),
#  - the deadlock-prone topology - a VLAN on a dummy, with the VLAN and the
#    dummy each enslaved to a different bond - can be built without hanging.

ALL_TESTS="
	vlan_link_state
	vlan_mtu
	vlan_features
	vlan_real_dev_enslave
"

REQUIRE_MZ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh

# Return 0 if $dev in netns $ns has flag $flag set (e.g. UP) in its <...> flags.
link_has_flag()
{
	local ns=$1 dev=$2 flag=$3

	ip -n "$ns" link show dev "$dev" 2>/dev/null | grep -q "[<,]${flag}[,>]"
}

link_lacks_flag()
{
	! link_has_flag "$@"
}

link_mtu_is()
{
	local ns=$1 dev=$2 want=$3 cur

	cur=$(ip -n "$ns" link show dev "$dev" 2>/dev/null | \
		sed -n 's/.* mtu \([0-9]\+\).*/\1/p')
	[ "$cur" = "$want" ]
}

vlan_feature_is()
{
	local ns=$1 dev=$2 feature=$3 value=$4

	ip netns exec "$ns" ethtool -k "$dev" 2>/dev/null | \
		grep -q "^$feature: $value"
}

link_has_master()
{
	local ns=$1 dev=$2 master=$3

	ip -n "$ns" -o link show dev "$dev" 2>/dev/null | grep -q "master $master"
}

vlan_link_state()
{
	RET=0

	ip -n "$NS" link add ls_dummy type dummy
	ip -n "$NS" link add link ls_dummy name ls_vlan type vlan id 100

	# Bringing the real device up must propagate UP to the VLAN.
	ip -n "$NS" link set ls_dummy up
	busywait "$BUSYWAIT_TIMEOUT" link_has_flag "$NS" ls_vlan UP
	check_err $? "VLAN did not go UP after the real device went UP"

	# ... and likewise for DOWN.
	ip -n "$NS" link set ls_dummy down
	busywait "$BUSYWAIT_TIMEOUT" link_lacks_flag "$NS" ls_vlan UP
	check_err $? "VLAN did not go DOWN after the real device went DOWN"

	ip -n "$NS" link del ls_vlan
	ip -n "$NS" link del ls_dummy

	log_test "VLAN link state follows the real device"
}

vlan_mtu()
{
	RET=0

	# The VLAN inherits the real device's MTU (2000) at creation time.
	ip -n "$NS" link add mtu_dummy mtu 2000 type dummy
	ip -n "$NS" link add link mtu_dummy name mtu_vlan type vlan id 100

	# Shrinking the real device's MTU must clamp the VLAN's MTU.
	ip -n "$NS" link set mtu_dummy mtu 1500
	busywait "$BUSYWAIT_TIMEOUT" link_mtu_is "$NS" mtu_vlan 1500
	check_err $? "VLAN MTU not clamped after the real device's MTU shrank"

	ip -n "$NS" link del mtu_vlan
	ip -n "$NS" link del mtu_dummy

	log_test "VLAN MTU clamped to the real device"
}

vlan_features()
{
	RET=0

	# Use veth as the real device: unlike dummy it exports vlan_features, so
	# the VLAN actually inherits a toggleable offload to assert on.
	ip -n "$NS" link add ft_veth type veth peer name ft_veth_pr
	ip -n "$NS" link add link ft_veth name ft_vlan type vlan id 100

	vlan_feature_is "$NS" ft_vlan scatter-gather on
	check_err $? "VLAN did not inherit scatter-gather from the real device"

	# Toggling the offload on the real device must propagate to the VLAN.
	ip netns exec "$NS" ethtool -K ft_veth sg off
	busywait "$BUSYWAIT_TIMEOUT" \
		vlan_feature_is "$NS" ft_vlan scatter-gather off
	check_err $? "VLAN scatter-gather still on after disabling it on real dev"

	ip netns exec "$NS" ethtool -K ft_veth sg on
	busywait "$BUSYWAIT_TIMEOUT" \
		vlan_feature_is "$NS" ft_vlan scatter-gather on
	check_err $? "VLAN scatter-gather still off after enabling it on real dev"

	ip -n "$NS" link del ft_vlan
	ip -n "$NS" link del ft_veth

	log_test "VLAN features follow the real device"
}

vlan_real_dev_enslave()
{
	RET=0

	# dummy <- VLAN -> bond0, then enslave the dummy itself to bond1.  The
	# last step brings the dummy up under bond1's instance lock, which used
	# to deadlock while synchronously propagating UP to the (bond-enslaved)
	# VLAN on top.
	ip -n "$NS" link add dl_dummy type dummy
	ip -n "$NS" link set dl_dummy up
	ip -n "$NS" link add link dl_dummy name dl_vlan type vlan id 100

	ip -n "$NS" link add dl_bond0 type bond mode active-backup
	ip -n "$NS" link set dl_vlan down
	ip -n "$NS" link set dl_vlan master dl_bond0
	check_err $? "could not enslave the VLAN to bond0"

	ip -n "$NS" link add dl_bond1 type bond mode active-backup
	ip -n "$NS" link set dl_dummy down
	ip -n "$NS" link set dl_dummy master dl_bond1
	check_err $? "could not enslave the real device to bond1"

	# If we got here the kernel did not deadlock; make sure it is still
	# responsive and the enslave really took effect.
	link_has_master "$NS" dl_dummy dl_bond1
	check_err $? "real device not enslaved to bond1"

	ip -n "$NS" link del dl_bond1
	ip -n "$NS" link del dl_bond0
	ip -n "$NS" link del dl_vlan
	ip -n "$NS" link del dl_dummy

	log_test "VLAN real device enslaved to a second bond"
}

setup_ns NS
trap 'cleanup_ns $NS' EXIT

tests_run

exit "$EXIT_STATUS"