#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Check xfrm policy resolution.  Topology:
#
# 1.2   1.1   3.1  3.10    2.1   2.2
# eth1  eth1 veth0 veth0 eth1   eth1
# ns1 ---- ns3 ----- ns4 ---- ns2
#
# ns3 and ns4 are connected via ipsec tunnel.
# pings from ns1 to ns2 (and vice versa) are supposed to work like this:
# ns1: ping 10.0.2.2: passes via ipsec tunnel.
# ns2: ping 10.0.1.2: passes via ipsec tunnel.

# ns1: ping 10.0.1.253: passes via ipsec tunnel (direct policy)
# ns2: ping 10.0.2.253: passes via ipsec tunnel (direct policy)
#
# ns1: ping 10.0.2.254: does NOT pass via ipsec tunnel (exception)
# ns2: ping 10.0.1.254: does NOT pass via ipsec tunnel (exception)

source lib.sh
ret=0
policy_checks_ok=1

KEY_SHA=0xdeadbeef1234567890abcdefabcdefabcdefabcd
KEY_AES=0x0123456789abcdef0123456789012345
SPI1=0x1
SPI2=0x2

do_esp_policy() {
    local ns=$1
    local me=$2
    local remote=$3
    local lnet=$4
    local rnet=$5

    # to encrypt packets as they go out (includes forwarded packets that need encapsulation)
    ip -net $ns xfrm policy add src $lnet dst $rnet dir out tmpl src $me dst $remote proto esp mode tunnel priority 100 action allow
    # to fwd decrypted packets after esp processing:
    ip -net $ns xfrm policy add src $rnet dst $lnet dir fwd tmpl src $remote dst $me proto esp mode tunnel priority 100 action allow
}

do_esp() {
    local ns=$1
    local me=$2
    local remote=$3
    local lnet=$4
    local rnet=$5
    local spi_out=$6
    local spi_in=$7

    ip -net $ns xfrm state add src $remote dst $me proto esp spi $spi_in  enc aes $KEY_AES  auth sha1 $KEY_SHA  mode tunnel sel src $rnet dst $lnet
    ip -net $ns xfrm state add src $me  dst $remote proto esp spi $spi_out enc aes $KEY_AES auth sha1 $KEY_SHA mode tunnel sel src $lnet dst $rnet

    do_esp_policy $ns $me $remote $lnet $rnet
}

# add policies with different netmasks, to make sure kernel carries
# the policies contained within new netmask over when search tree is
# re-built.
# peer netns that are supposed to be encapsulated via esp have addresses
# in the 10.0.1.0/24 and 10.0.2.0/24 subnets, respectively.
#
# Adding a policy for '10.0.1.0/23' will make it necessary to
# alter the prefix of 10.0.1.0 subnet.
# In case new prefix overlaps with existing node, the node and all
# policies it carries need to be merged with the existing one(s).
#
# Do that here.
do_overlap()
{
    local ns=$1

    # adds new nodes to tree (neither network exists yet in policy database).
    ip -net $ns xfrm policy add src 10.1.0.0/24 dst 10.0.0.0/24 dir fwd priority 200 action block

    # adds a new node in the 10.0.0.0/24 tree (dst node exists).
    ip -net $ns xfrm policy add src 10.2.0.0/24 dst 10.0.0.0/24 dir fwd priority 200 action block

    # adds a 10.2.0.0/23 node, but for different dst.
    ip -net $ns xfrm policy add src 10.2.0.0/23 dst 10.0.1.0/24 dir fwd priority 200 action block

    # dst now overlaps with the 10.0.1.0/24 ESP policy in fwd.
    # kernel must 'promote' existing one (10.0.0.0/24) to 10.0.0.0/23.
    # But 10.0.0.0/23 also includes existing 10.0.1.0/24, so that node
    # also has to be merged too, including source-sorted subtrees.
    # old:
    # 10.0.0.0/24 (node 1 in dst tree of the bin)
    #    10.1.0.0/24 (node in src tree of dst node 1)
    #    10.2.0.0/24 (node in src tree of dst node 1)
    # 10.0.1.0/24 (node 2 in dst tree of the bin)
    #    10.0.2.0/24 (node in src tree of dst node 2)
    #    10.2.0.0/24 (node in src tree of dst node 2)
    #
    # The next 'policy add' adds dst '10.0.0.0/23', which means
    # that dst node 1 and dst node 2 have to be merged including
    # the sub-tree.  As no duplicates are allowed, policies in
    # the two '10.0.2.0/24' are also merged.
    #
    # after the 'add', internal search tree should look like this:
    # 10.0.0.0/23 (node in dst tree of bin)
    #     10.0.2.0/24 (node in src tree of dst node)
    #     10.1.0.0/24 (node in src tree of dst node)
    #     10.2.0.0/24 (node in src tree of dst node)
    #
    # 10.0.0.0/24 and 10.0.1.0/24 nodes have been merged as 10.0.0.0/23.
    ip -net $ns xfrm policy add src 10.1.0.0/24 dst 10.0.0.0/23 dir fwd priority 200 action block

    # similar to above: add policies (with partially random address), with shrinking prefixes.
    for p in 29 28 27;do
      for k in $(seq 1 32); do
       ip -net $ns xfrm policy add src 10.253.1.$((RANDOM%255))/$p dst 10.254.1.$((RANDOM%255))/$p dir fwd priority $((200+k)) action block 2>/dev/null
      done
    done
}

do_esp_policy_get_check() {
    local ns=$1
    local lnet=$2
    local rnet=$3

    ip -net $ns xfrm policy get src $lnet dst $rnet dir out > /dev/null
    if [ $? -ne 0 ] && [ $policy_checks_ok -eq 1 ] ;then
        policy_checks_ok=0
        echo "FAIL: ip -net $ns xfrm policy get src $lnet dst $rnet dir out"
        ret=1
    fi

    ip -net $ns xfrm policy get src $rnet dst $lnet dir fwd > /dev/null
    if [ $? -ne 0 ] && [ $policy_checks_ok -eq 1 ] ;then
        policy_checks_ok=0
        echo "FAIL: ip -net $ns xfrm policy get src $rnet dst $lnet dir fwd"
        ret=1
    fi
}

do_exception() {
    local ns=$1
    local me=$2
    local remote=$3
    local encryptip=$4
    local plain=$5

    # network $plain passes without tunnel
    ip -net $ns xfrm policy add dst $plain dir out priority 10 action allow

    # direct policy for $encryptip, use tunnel, higher prio takes precedence
    ip -net $ns xfrm policy add dst $encryptip dir out tmpl src $me dst $remote proto esp mode tunnel priority 1 action allow
}

# policies that are not supposed to match any packets generated in this test.
do_dummies4() {
    local ns=$1

    for i in $(seq 10 16);do
      # dummy policy with wildcard src/dst.
      echo netns exec $ns ip xfrm policy add src 0.0.0.0/0 dst 10.$i.99.0/30 dir out action block
      echo netns exec $ns ip xfrm policy add src 10.$i.99.0/30 dst 0.0.0.0/0 dir out action block
      for j in $(seq 32 64);do
        echo netns exec $ns ip xfrm policy add src 10.$i.1.0/30 dst 10.$i.$j.0/30 dir out action block
        # silly, as it encompasses the one above too, but its allowed:
        echo netns exec $ns ip xfrm policy add src 10.$i.1.0/29 dst 10.$i.$j.0/29 dir out action block
        # and yet again, even more broad one.
        echo netns exec $ns ip xfrm policy add src 10.$i.1.0/24 dst 10.$i.$j.0/24 dir out action block
        echo netns exec $ns ip xfrm policy add src 10.$i.$j.0/24 dst 10.$i.1.0/24 dir fwd action block
      done
    done | ip -batch /dev/stdin
}

do_dummies6() {
    local ns=$1

    for i in $(seq 10 16);do
      for j in $(seq 32 64);do
       echo netns exec $ns ip xfrm policy add src dead:$i::/64 dst dead:$i:$j::/64 dir out action block
       echo netns exec $ns ip xfrm policy add src dead:$i:$j::/64 dst dead:$i::/24 dir fwd action block
      done
    done | ip -batch /dev/stdin
}

check_ipt_policy_count()
{
	ns=$1

	ip netns exec $ns iptables-save -c |grep policy | ( read c rest
		ip netns exec $ns iptables -Z
		if [ x"$c" = x'[0:0]' ]; then
			exit 0
		elif [ x"$c" = x ]; then
			echo "ERROR: No counters"
			ret=1
			exit 111
		else
			exit 1
		fi
	)
}

check_xfrm() {
	# 0: iptables -m policy rule count == 0
	# 1: iptables -m policy rule count != 0
	rval=$1
	ip=$2
	local lret=0

	ip netns exec ${ns[1]} ping -q -c 1 10.0.2.$ip > /dev/null

	check_ipt_policy_count ${ns[3]}
	if [ $? -ne $rval ] ; then
		lret=1
	fi
	check_ipt_policy_count ${ns[4]}
	if [ $? -ne $rval ] ; then
		lret=1
	fi

	ip netns exec ${ns[2]} ping -q -c 1 10.0.1.$ip > /dev/null

	check_ipt_policy_count ${ns[3]}
	if [ $? -ne $rval ] ; then
		lret=1
	fi
	check_ipt_policy_count ${ns[4]}
	if [ $? -ne $rval ] ; then
		lret=1
	fi

	return $lret
}

check_exceptions()
{
	logpostfix="$1"
	local lret=0

	# ping to .254 should be excluded from the tunnel (exception is in place).
	check_xfrm 0 254
	if [ $? -ne 0 ]; then
		echo "FAIL: expected ping to .254 to fail ($logpostfix)"
		lret=1
	else
		echo "PASS: ping to .254 bypassed ipsec tunnel ($logpostfix)"
	fi

	# ping to .253 should use use ipsec due to direct policy exception.
	check_xfrm 1 253
	if [ $? -ne 0 ]; then
		echo "FAIL: expected ping to .253 to use ipsec tunnel ($logpostfix)"
		lret=1
	else
		echo "PASS: direct policy matches ($logpostfix)"
	fi

	# ping to .2 should use ipsec.
	check_xfrm 1 2
	if [ $? -ne 0 ]; then
		echo "FAIL: expected ping to .2 to use ipsec tunnel ($logpostfix)"
		lret=1
	else
		echo "PASS: policy matches ($logpostfix)"
	fi

	return $lret
}

check_hthresh_repeat()
{
	local log=$1
	i=0

	for i in $(seq 1 10);do
		ip -net ${ns[1]} xfrm policy update src e000:0001::0000 dst ff01::0014:0000:0001 dir in tmpl src :: dst :: proto esp mode tunnel priority 100 action allow || break
		ip -net ${ns[1]} xfrm policy set hthresh6 0 28 || break

		ip -net ${ns[1]} xfrm policy update src e000:0001::0000 dst ff01::01 dir in tmpl src :: dst :: proto esp mode tunnel priority 100 action allow || break
		ip -net ${ns[1]} xfrm policy set hthresh6 0 28 || break
	done

	if [ $i -ne 10 ] ;then
		echo "FAIL: $log" 1>&2
		ret=1
		return 1
	fi

	echo "PASS: $log"
	return 0
}

# insert non-overlapping policies in a random order and check that
# all of them can be fetched using the traffic selectors.
check_random_order()
{
	local ns=$1
	local log=$2

	for i in $(seq 50); do
		ip -net $ns xfrm policy flush
		for j in $(seq 0 16 255 | sort -R); do
			ip -net $ns xfrm policy add dst $j.0.0.0/24 dir out priority 10 action allow
		done
		for j in $(seq 0 16 255); do
			if ! ip -net $ns xfrm policy get dst $j.0.0.0/24 dir out > /dev/null; then
				echo "FAIL: $log" 1>&2
				return 1
			fi
		done
	done

	for i in $(seq 50); do
		ip -net $ns xfrm policy flush
		for j in $(seq 0 16 255 | sort -R); do
			local addr=$(printf "e000:0000:%02x00::/56" $j)
			ip -net $ns xfrm policy add dst $addr dir out priority 10 action allow
		done
		for j in $(seq 0 16 255); do
			local addr=$(printf "e000:0000:%02x00::/56" $j)
			if ! ip -net $ns xfrm policy get dst $addr dir out > /dev/null; then
				echo "FAIL: $log" 1>&2
				return 1
			fi
		done
	done

	ip -net $ns xfrm policy flush

	echo "PASS: $log"
	return 0
}

#check for needed privileges
if [ "$(id -u)" -ne 0 ];then
	echo "SKIP: Need root privileges"
	exit $ksft_skip
fi

ip -Version 2>/dev/null >/dev/null
if [ $? -ne 0 ];then
	echo "SKIP: Could not run test without the ip tool"
	exit $ksft_skip
fi

# needed to check if policy lookup got valid ipsec result
iptables --version 2>/dev/null >/dev/null
if [ $? -ne 0 ];then
	echo "SKIP: Could not run test without iptables tool"
	exit $ksft_skip
fi

setup_ns ns1 ns2 ns3 ns4
ns[1]=$ns1
ns[2]=$ns2
ns[3]=$ns3
ns[4]=$ns4

DEV=veth0
ip link add $DEV netns ${ns[1]} type veth peer name eth1 netns ${ns[3]}
ip link add $DEV netns ${ns[2]} type veth peer name eth1 netns ${ns[4]}

ip link add $DEV netns ${ns[3]} type veth peer name veth0 netns ${ns[4]}

DEV=veth0
for i in 1 2; do
    ip -net ${ns[$i]} link set $DEV up
    ip -net ${ns[$i]} addr add 10.0.$i.2/24 dev $DEV
    ip -net ${ns[$i]} addr add dead:$i::2/64 dev $DEV

    ip -net ${ns[$i]} addr add 10.0.$i.253 dev $DEV
    ip -net ${ns[$i]} addr add 10.0.$i.254 dev $DEV
    ip -net ${ns[$i]} addr add dead:$i::fd dev $DEV
    ip -net ${ns[$i]} addr add dead:$i::fe dev $DEV
done

for i in 3 4; do
    ip -net ${ns[$i]} link set eth1 up
    ip -net ${ns[$i]} link set veth0 up
done

ip -net ${ns[1]} route add default via 10.0.1.1
ip -net ${ns[2]} route add default via 10.0.2.1

ip -net ${ns[3]} addr add 10.0.1.1/24 dev eth1
ip -net ${ns[3]} addr add 10.0.3.1/24 dev veth0
ip -net ${ns[3]} addr add 2001:1::1/64 dev eth1
ip -net ${ns[3]} addr add 2001:3::1/64 dev veth0

ip -net ${ns[3]} route add default via 10.0.3.10

ip -net ${ns[4]} addr add 10.0.2.1/24 dev eth1
ip -net ${ns[4]} addr add 10.0.3.10/24 dev veth0
ip -net ${ns[4]} addr add 2001:2::1/64 dev eth1
ip -net ${ns[4]} addr add 2001:3::10/64 dev veth0
ip -net ${ns[4]} route add default via 10.0.3.1

for j in 4 6; do
	for i in 3 4;do
		ip netns exec ${ns[$i]} sysctl net.ipv$j.conf.eth1.forwarding=1 > /dev/null
		ip netns exec ${ns[$i]} sysctl net.ipv$j.conf.veth0.forwarding=1 > /dev/null
	done
done

# abuse iptables rule counter to check if ping matches a policy
ip netns exec ${ns[3]} iptables -p icmp -A FORWARD -m policy --dir out --pol ipsec
ip netns exec ${ns[4]} iptables -p icmp -A FORWARD -m policy --dir out --pol ipsec
if [ $? -ne 0 ];then
	echo "SKIP: Could not insert iptables rule"
	cleanup_ns $ns1 $ns2 $ns3 $ns4
	exit $ksft_skip
fi

#          localip  remoteip  localnet    remotenet
do_esp ${ns[3]} 10.0.3.1 10.0.3.10 10.0.1.0/24 10.0.2.0/24 $SPI1 $SPI2
do_esp ${ns[3]} dead:3::1 dead:3::10 dead:1::/64 dead:2::/64 $SPI1 $SPI2
do_esp ${ns[4]} 10.0.3.10 10.0.3.1 10.0.2.0/24 10.0.1.0/24 $SPI2 $SPI1
do_esp ${ns[4]} dead:3::10 dead:3::1 dead:2::/64 dead:1::/64 $SPI2 $SPI1

do_dummies4 ${ns[3]}
do_dummies6 ${ns[4]}

do_esp_policy_get_check ${ns[3]} 10.0.1.0/24 10.0.2.0/24
do_esp_policy_get_check ${ns[4]} 10.0.2.0/24 10.0.1.0/24
do_esp_policy_get_check ${ns[3]} dead:1::/64 dead:2::/64
do_esp_policy_get_check ${ns[4]} dead:2::/64 dead:1::/64

# ping to .254 should use ipsec, exception is not installed.
check_xfrm 1 254
if [ $? -ne 0 ]; then
	echo "FAIL: expected ping to .254 to use ipsec tunnel"
	ret=1
else
	echo "PASS: policy before exception matches"
fi

# installs exceptions
#                localip  remoteip   encryptdst  plaindst
do_exception ${ns[3]} 10.0.3.1 10.0.3.10 10.0.2.253 10.0.2.240/28
do_exception ${ns[4]} 10.0.3.10 10.0.3.1 10.0.1.253 10.0.1.240/28

do_exception ${ns[3]} dead:3::1 dead:3::10 dead:2::fd  dead:2:f0::/96
do_exception ${ns[4]} dead:3::10 dead:3::1 dead:1::fd  dead:1:f0::/96

check_exceptions "exceptions"
if [ $? -ne 0 ]; then
	ret=1
fi

# insert block policies with adjacent/overlapping netmasks
do_overlap ${ns[3]}

check_exceptions "exceptions and block policies"
if [ $? -ne 0 ]; then
	ret=1
fi

for n in ${ns[3]} ${ns[4]};do
	ip -net $n xfrm policy set hthresh4 28 24 hthresh6 126 125
	sleep $((RANDOM%5))
done

check_exceptions "exceptions and block policies after hresh changes"

# full flush of policy db, check everything gets freed incl. internal meta data
ip -net ${ns[3]} xfrm policy flush

do_esp_policy ${ns[3]} 10.0.3.1 10.0.3.10 10.0.1.0/24 10.0.2.0/24
do_exception ${ns[3]} 10.0.3.1 10.0.3.10 10.0.2.253 10.0.2.240/28

# move inexact policies to hash table
ip -net ${ns[3]} xfrm policy set hthresh4 16 16

sleep $((RANDOM%5))
check_exceptions "exceptions and block policies after hthresh change in ns3"

# restore original hthresh settings -- move policies back to tables
for n in ${ns[3]} ${ns[4]};do
	ip -net $n xfrm policy set hthresh4 32 32 hthresh6 128 128
	sleep $((RANDOM%5))
done
check_exceptions "exceptions and block policies after htresh change to normal"

check_hthresh_repeat "policies with repeated htresh change"

check_random_order ${ns[3]} "policies inserted in random order"

cleanup_ns $ns1 $ns2 $ns3 $ns4

exit $ret
