#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Various combinations of VRF with xfrms and qdisc.

source lib.sh
PAUSE_ON_FAIL=no
VERBOSE=0
ret=0

HOST1_4=192.168.1.1
HOST2_4=192.168.1.2
HOST1_6=2001:db8:1::1
HOST2_6=2001:db8:1::2

XFRM1_4=10.0.1.1
XFRM2_4=10.0.1.2
XFRM1_6=fc00:1000::1
XFRM2_6=fc00:1000::2
IF_ID=123

VRF=red
TABLE=300

AUTH_1=0xd94fcfea65fddf21dc6e0d24a0253508
AUTH_2=0xdc6e0d24a0253508d94fcfea65fddf21
ENC_1=0xfc46c20f8048be9725930ff3fb07ac2a91f0347dffeacf62
ENC_2=0x3fb07ac2a91f0347dffeacf62fc46c20f8048be9725930ff
SPI_1=0x02122b77
SPI_2=0x2b770212

which ping6 > /dev/null 2>&1 && ping6=$(which ping6) || ping6=$(which ping)

################################################################################
#
log_test()
{
	local rc=$1
	local expected=$2
	local msg="$3"

	if [ ${rc} -eq ${expected} ]; then
		printf "TEST: %-60s  [ OK ]\n" "${msg}"
		nsuccess=$((nsuccess+1))
	else
		ret=1
		nfail=$((nfail+1))
		printf "TEST: %-60s  [FAIL]\n" "${msg}"
		if [ "${PAUSE_ON_FAIL}" = "yes" ]; then
			echo
			echo "hit enter to continue, 'q' to quit"
			read a
			[ "$a" = "q" ] && exit 1
		fi
	fi
}

run_cmd_host1()
{
	local cmd="$*"
	local out
	local rc

	if [ "$VERBOSE" = "1" ]; then
		printf "    COMMAND: $cmd\n"
	fi

	out=$(eval ip netns exec $host1 $cmd 2>&1)
	rc=$?
	if [ "$VERBOSE" = "1" ]; then
		if [ -n "$out" ]; then
			echo
			echo "    $out"
		fi
		echo
	fi

	return $rc
}

################################################################################
# create namespaces for hosts and sws

create_vrf()
{
	local ns=$1
	local vrf=$2
	local table=$3

	if [ -n "${ns}" ]; then
		ns="-netns ${ns}"
	fi

	ip ${ns} link add ${vrf} type vrf table ${table}
	ip ${ns} link set ${vrf} up
	ip ${ns} route add vrf ${vrf} unreachable default metric 8192
	ip ${ns} -6 route add vrf ${vrf} unreachable default metric 8192

	ip ${ns} addr add 127.0.0.1/8 dev ${vrf}
	ip ${ns} -6 addr add ::1 dev ${vrf} nodad

	ip ${ns} ru del pref 0
	ip ${ns} ru add pref 32765 from all lookup local
	ip ${ns} -6 ru del pref 0
	ip ${ns} -6 ru add pref 32765 from all lookup local
}

create_ns()
{
	local ns=$1
	local addr=$2
	local addr6=$3

	[ -z "${addr}" ] && addr="-"
	[ -z "${addr6}" ] && addr6="-"

	if [ "${addr}" != "-" ]; then
		ip -netns ${ns} addr add dev lo ${addr}
	fi
	if [ "${addr6}" != "-" ]; then
		ip -netns ${ns} -6 addr add dev lo ${addr6}
	fi

	ip -netns ${ns} ro add unreachable default metric 8192
	ip -netns ${ns} -6 ro add unreachable default metric 8192

	ip netns exec ${ns} sysctl -qw net.ipv4.ip_forward=1
	ip netns exec ${ns} sysctl -qw net.ipv6.conf.all.keep_addr_on_down=1
	ip netns exec ${ns} sysctl -qw net.ipv6.conf.all.forwarding=1
	ip netns exec ${ns} sysctl -qw net.ipv6.conf.default.forwarding=1
	ip netns exec ${ns} sysctl -qw net.ipv6.conf.default.accept_dad=0
}

# create veth pair to connect namespaces and apply addresses.
connect_ns()
{
	local ns1=$1
	local ns1_dev=$2
	local ns1_addr=$3
	local ns1_addr6=$4
	local ns2=$5
	local ns2_dev=$6
	local ns2_addr=$7
	local ns2_addr6=$8
	local ns1arg
	local ns2arg

	if [ -n "${ns1}" ]; then
		ns1arg="-netns ${ns1}"
	fi
	if [ -n "${ns2}" ]; then
		ns2arg="-netns ${ns2}"
	fi

	ip ${ns1arg} li add ${ns1_dev} type veth peer name tmp
	ip ${ns1arg} li set ${ns1_dev} up
	ip ${ns1arg} li set tmp netns ${ns2} name ${ns2_dev}
	ip ${ns2arg} li set ${ns2_dev} up

	if [ "${ns1_addr}" != "-" ]; then
		ip ${ns1arg} addr add dev ${ns1_dev} ${ns1_addr}
		ip ${ns2arg} addr add dev ${ns2_dev} ${ns2_addr}
	fi

	if [ "${ns1_addr6}" != "-" ]; then
		ip ${ns1arg} addr add dev ${ns1_dev} ${ns1_addr6} nodad
		ip ${ns2arg} addr add dev ${ns2_dev} ${ns2_addr6} nodad
	fi
}

################################################################################

cleanup()
{
	cleanup_ns $host1 $host2
}

setup()
{
	setup_ns host1 host2
	create_ns "$host1"
	create_ns "$host2"

	connect_ns "$host1" eth0 ${HOST1_4}/24 ${HOST1_6}/64 \
	           "$host2" eth0 ${HOST2_4}/24 ${HOST2_6}/64

	create_vrf "$host1" ${VRF} ${TABLE}
	ip -netns $host1 link set dev eth0 master ${VRF}
}

cleanup_xfrm()
{
	for ns in $host1 $host2
	do
		for x in state policy
		do
			ip -netns ${ns} xfrm ${x} flush
			ip -6 -netns ${ns} xfrm ${x} flush
		done
	done
}

setup_xfrm()
{
	local h1_4=$1
	local h2_4=$2
	local h1_6=$3
	local h2_6=$4
	local devarg="$5"

	#
	# policy
	#

	# host1 - IPv4 out
	ip -netns $host1 xfrm policy add \
	  src ${h1_4} dst ${h2_4} ${devarg} dir out \
	  tmpl src ${HOST1_4} dst ${HOST2_4} proto esp mode tunnel

	# host2 - IPv4 in
	ip -netns $host2 xfrm policy add \
	  src ${h1_4} dst ${h2_4} dir in \
	  tmpl src ${HOST1_4} dst ${HOST2_4} proto esp mode tunnel

	# host1 - IPv4 in
	ip -netns $host1 xfrm policy add \
	  src ${h2_4} dst ${h1_4} ${devarg} dir in \
	  tmpl src ${HOST2_4} dst ${HOST1_4} proto esp mode tunnel

	# host2 - IPv4 out
	ip -netns $host2 xfrm policy add \
	  src ${h2_4} dst ${h1_4} dir out \
	  tmpl src ${HOST2_4} dst ${HOST1_4} proto esp mode tunnel


	# host1 - IPv6 out
	ip -6 -netns $host1 xfrm policy add \
	  src ${h1_6} dst ${h2_6} ${devarg} dir out \
	  tmpl src ${HOST1_6} dst ${HOST2_6} proto esp mode tunnel

	# host2 - IPv6 in
	ip -6 -netns $host2 xfrm policy add \
	  src ${h1_6} dst ${h2_6} dir in \
	  tmpl src ${HOST1_6} dst ${HOST2_6} proto esp mode tunnel

	# host1 - IPv6 in
	ip -6 -netns $host1 xfrm policy add \
	  src ${h2_6} dst ${h1_6} ${devarg} dir in \
	  tmpl src ${HOST2_6} dst ${HOST1_6} proto esp mode tunnel

	# host2 - IPv6 out
	ip -6 -netns $host2 xfrm policy add \
	  src ${h2_6} dst ${h1_6} dir out \
	  tmpl src ${HOST2_6} dst ${HOST1_6} proto esp mode tunnel

	#
	# state
	#
	ip -netns $host1 xfrm state add src ${HOST1_4} dst ${HOST2_4} \
	    proto esp spi ${SPI_1} reqid 0 mode tunnel \
	    replay-window 4 replay-oseq 0x4 \
	    auth-trunc 'hmac(sha1)' ${AUTH_1} 96 \
	    enc 'cbc(aes)' ${ENC_1} \
	    sel src ${h1_4} dst ${h2_4} ${devarg}

	ip -netns $host2 xfrm state add src ${HOST1_4} dst ${HOST2_4} \
	    proto esp spi ${SPI_1} reqid 0 mode tunnel \
	    replay-window 4 replay-oseq 0x4 \
	    auth-trunc 'hmac(sha1)' ${AUTH_1} 96 \
	    enc 'cbc(aes)' ${ENC_1} \
	    sel src ${h1_4} dst ${h2_4}


	ip -netns $host1 xfrm state add src ${HOST2_4} dst ${HOST1_4} \
	    proto esp spi ${SPI_2} reqid 0 mode tunnel \
	    replay-window 4 replay-oseq 0x4 \
	    auth-trunc 'hmac(sha1)' ${AUTH_2} 96 \
	    enc 'cbc(aes)' ${ENC_2} \
	    sel src ${h2_4} dst ${h1_4} ${devarg}

	ip -netns $host2 xfrm state add src ${HOST2_4} dst ${HOST1_4} \
	    proto esp spi ${SPI_2} reqid 0 mode tunnel \
	    replay-window 4 replay-oseq 0x4 \
	    auth-trunc 'hmac(sha1)' ${AUTH_2} 96 \
	    enc 'cbc(aes)' ${ENC_2} \
	    sel src ${h2_4} dst ${h1_4}


	ip -6 -netns $host1 xfrm state add src ${HOST1_6} dst ${HOST2_6} \
	    proto esp spi ${SPI_1} reqid 0 mode tunnel \
	    replay-window 4 replay-oseq 0x4 \
	    auth-trunc 'hmac(sha1)' ${AUTH_1} 96 \
	    enc 'cbc(aes)' ${ENC_1} \
	    sel src ${h1_6} dst ${h2_6} ${devarg}

	ip -6 -netns $host2 xfrm state add src ${HOST1_6} dst ${HOST2_6} \
	    proto esp spi ${SPI_1} reqid 0 mode tunnel \
	    replay-window 4 replay-oseq 0x4 \
	    auth-trunc 'hmac(sha1)' ${AUTH_1} 96 \
	    enc 'cbc(aes)' ${ENC_1} \
	    sel src ${h1_6} dst ${h2_6}


	ip -6 -netns $host1 xfrm state add src ${HOST2_6} dst ${HOST1_6} \
	    proto esp spi ${SPI_2} reqid 0 mode tunnel \
	    replay-window 4 replay-oseq 0x4 \
	    auth-trunc 'hmac(sha1)' ${AUTH_2} 96 \
	    enc 'cbc(aes)' ${ENC_2} \
	    sel src ${h2_6} dst ${h1_6} ${devarg}

	ip -6 -netns $host2 xfrm state add src ${HOST2_6} dst ${HOST1_6} \
	    proto esp spi ${SPI_2} reqid 0 mode tunnel \
	    replay-window 4 replay-oseq 0x4 \
	    auth-trunc 'hmac(sha1)' ${AUTH_2} 96 \
	    enc 'cbc(aes)' ${ENC_2} \
	    sel src ${h2_6} dst ${h1_6}
}

cleanup_xfrm_dev()
{
	ip -netns $host1 li del xfrm0
	ip -netns $host2 addr del ${XFRM2_4}/24 dev eth0
	ip -netns $host2 addr del ${XFRM2_6}/64 dev eth0
}

setup_xfrm_dev()
{
	local vrfarg="vrf ${VRF}"

	ip -netns $host1 li add type xfrm dev eth0 if_id ${IF_ID}
	ip -netns $host1 li set xfrm0 ${vrfarg} up
	ip -netns $host1 addr add ${XFRM1_4}/24 dev xfrm0
	ip -netns $host1 addr add ${XFRM1_6}/64 dev xfrm0

	ip -netns $host2 addr add ${XFRM2_4}/24 dev eth0
	ip -netns $host2 addr add ${XFRM2_6}/64 dev eth0

	setup_xfrm ${XFRM1_4} ${XFRM2_4} ${XFRM1_6} ${XFRM2_6} "if_id ${IF_ID}"
}

run_tests()
{
	cleanup_xfrm

	# no IPsec
	run_cmd_host1 ip vrf exec ${VRF} ping -c1 -w1 ${HOST2_4}
	log_test $? 0 "IPv4 no xfrm policy"
	run_cmd_host1 ip vrf exec ${VRF} ${ping6} -c1 -w1 ${HOST2_6}
	log_test $? 0 "IPv6 no xfrm policy"

	# xfrm without VRF in sel
	setup_xfrm ${HOST1_4} ${HOST2_4} ${HOST1_6} ${HOST2_6}
	run_cmd_host1 ip vrf exec ${VRF} ping -c1 -w1 ${HOST2_4}
	log_test $? 0 "IPv4 xfrm policy based on address"
	run_cmd_host1 ip vrf exec ${VRF} ${ping6} -c1 -w1 ${HOST2_6}
	log_test $? 0 "IPv6 xfrm policy based on address"
	cleanup_xfrm

	# xfrm with VRF in sel
	# Known failure: ipv4 resets the flow oif after the lookup. Fix is
	# not straightforward.
	# setup_xfrm ${HOST1_4} ${HOST2_4} ${HOST1_6} ${HOST2_6} "dev ${VRF}"
	# run_cmd_host1 ip vrf exec ${VRF} ping -c1 -w1 ${HOST2_4}
	# log_test $? 0 "IPv4 xfrm policy with VRF in selector"
	run_cmd_host1 ip vrf exec ${VRF} ${ping6} -c1 -w1 ${HOST2_6}
	log_test $? 0 "IPv6 xfrm policy with VRF in selector"
	cleanup_xfrm

	# xfrm with enslaved device in sel
	# Known failures: combined with the above, __xfrm{4,6}_selector_match
	# needs to consider both l3mdev and enslaved device index.
	# setup_xfrm ${HOST1_4} ${HOST2_4} ${HOST1_6} ${HOST2_6} "dev eth0"
	# run_cmd_host1 ip vrf exec ${VRF} ping -c1 -w1 ${HOST2_4}
	# log_test $? 0 "IPv4 xfrm policy with enslaved device in selector"
	# run_cmd_host1 ip vrf exec ${VRF} ${ping6} -c1 -w1 ${HOST2_6}
	# log_test $? 0 "IPv6 xfrm policy with enslaved device in selector"
	# cleanup_xfrm

	# xfrm device
	setup_xfrm_dev
	run_cmd_host1 ip vrf exec ${VRF} ping -c1 -w1 ${XFRM2_4}
	log_test $? 0 "IPv4 xfrm policy with xfrm device"
	run_cmd_host1 ip vrf exec ${VRF} ${ping6} -c1 -w1 ${XFRM2_6}
	log_test $? 0 "IPv6 xfrm policy with xfrm device"
	cleanup_xfrm_dev
}

################################################################################
# usage

usage()
{
        cat <<EOF
usage: ${0##*/} OPTS

        -p          Pause on fail
        -v          verbose mode (show commands and output)

done
EOF
}

################################################################################
# main

while getopts :pv o
do
	case $o in
		p) PAUSE_ON_FAIL=yes;;
		v) VERBOSE=$(($VERBOSE + 1));;
		h) usage; exit 0;;
		*) usage; exit 1;;
	esac
done

cleanup 2>/dev/null
setup

echo
echo "No qdisc on VRF device"
run_tests

run_cmd_host1 tc qdisc add dev ${VRF} root netem delay 100ms
echo
echo "netem qdisc on VRF device"
run_tests

printf "\nTests passed: %3d\n" ${nsuccess}
printf "Tests failed: %3d\n"   ${nfail}

exit $ret
