#!/bin/bash

# NFT_TEST_REQUIRES(NFT_TEST_HAVE_socat)

. $NFT_TEST_LIBRARY_FILE

set -x

rc=1

cleanup()
{
	set +e
	for i in $R $C $S;do
		kill $(ip netns pid $i) 2>/dev/null
		ip netns del $i
	done

	exit $rc
}
trap cleanup EXIT

dump_expect_table() {
	ip netns exec "$R" conntrack --family ipv6 -L expect
	ip netns exec "$R" conntrack --family ipv4 -L expect
}

assert_failout()
{
	ip netns exec $R $NFT list ruleset
	ip netns exec $R conntrack -L
	dump_expect_table
}

do_connect() {
	local ns="$1"
	local addr="$2"
	local port="$3"

	ip netns exec "$ns" socat -u STDIN TCP-CONNECT:$addr:$port,connect-timeout=1 < /dev/null
}

connect_fail() {
	local ns="$1"
	local addr="$2"
	local port="$3"
	local msg="$4"

	do_connect "$ns" "$addr" "$port"
	assert_fail "connect from $ns to $addr:$port $msg"
}

connect_ok() {
	local ns="$1"
	local addr="$2"
	local port="$3"
	local msg="$4"

	do_connect "$ns" "$addr" "$port"
	assert_pass "connect from $ns to $addr:$port $msg"
}

test_expect() {
	local ip_cr="$1"
	local ip_sr="$2"
	local msg="$3"

	echo check port is closed from outside.
	connect_fail "$S" "$ip_cr" 1234

	echo "Test: expectation via ruleset$msg"

	echo control port should be open.
	connect_ok "$C" "$ip_sr" 2222 "$msg"

	dump_expect_table

	echo expected port. Should now pass.
	connect_ok "$S" "$ip_cr" 1234 "$msg"

	echo one expectation max, expect fail.
	connect_fail "$S" "$ip_cr" 1234 "$msg"
}

test_timeout() {
	echo "Test: expectation auto-timeout"
	# control port - this should be open.
	connect_ok "$C" "$ip4_sr" 2222
	connect_ok "$C" "[$ip6_sr]" 2222

	dump_expect_table

	# timeout. We 'bundle' ipv4 and ipv6 test here.
	sleep 6

	dump_expect_table

	echo "expect failures"
	connect_fail "$S" "$ip4_cr" 1234
	connect_fail "$S" "[$ip6_cr]" 1234
}

rnd=$(mktemp -u XXXXXXXX)
R="ns-router-$rnd"
C="ns-client-$rnd"
S="ns-server-$rnd"

ip6_sr=dead:d8:ff:22::1
ip6_cr=dead:d8:ff:21::2
ip6_rs=dead:d8:ff:22::fffe
ip6_rc=dead:d8:ff:21::fffe

ip4_sr=10.1.22.1
ip4_cr=10.1.21.2
ip4_rs=10.1.22.99
ip4_rc=10.1.21.99

set -e
ip netns add $R
ip netns add $S
ip netns add $C
ip -net $S link set lo up
ip -net $R link set lo up
ip -net $C link set lo up
ip netns exec $R sysctl -wq net.ipv4.ip_forward=1
ip netns exec $R sysctl -wq net.ipv6.conf.all.forwarding=1

ip link add s_r netns $S type veth peer name r_s netns $R
ip link add c_r netns $C type veth peer name r_c netns $R
ip -net $S link set s_r up
ip -net $R link set r_s up
ip -net $R link set r_c up
ip -net $C link set c_r up

ip -net $S addr add ${ip6_sr}/64 dev s_r nodad
ip -net $C addr add ${ip6_cr}/64 dev c_r nodad
ip -net $R addr add ${ip6_rs}/64 dev r_s nodad
ip -net $R addr add ${ip6_rc}/64 dev r_c nodad
ip -net $C route add ${ip6_rs}/64 via ${ip6_rc} dev c_r
ip -net $S route add ${ip6_rc}/64 via ${ip6_rs} dev s_r
assert_pass "topo initialization"

ip -net $S addr add ${ip4_sr}/24 dev s_r
ip -net $C addr add ${ip4_cr}/24 dev c_r
ip -net $R addr add ${ip4_rs}/24 dev r_s
ip -net $R addr add ${ip4_rc}/24 dev r_c

ip -net $C route add 10.1.22.0/24 via ${ip4_rc} dev c_r
ip -net $S route add 10.1.21.0/24 via ${ip4_rs} dev s_r

ip netns exec "$C" ping -q -c 1 "$ip4_sr"
ip netns exec "$C" ping -q -c 1 "$ip6_sr"

timeout 20 ip netns exec "$S" socat TCP6-LISTEN:2222,ipv6only=1,fork,reuseaddr PIPE &
timeout 20 ip netns exec "$S" socat TCP4-LISTEN:2222,fork,reuseaddr PIPE &
timeout 20 ip netns exec "$C" socat TCP6-LISTEN:1234,ipv6only=1,fork,reuseaddr PIPE &
timeout 20 ip netns exec "$C" socat TCP4-LISTEN:1234,fork,reuseaddr PIPE &
wait_local_port_listen $S 2222 tcp
wait_local_port_listen $C 1234 tcp

ip netns exec $R $NFT -f - <<-EOF
flush ruleset
table inet t {
	ct expectation ctexpect4 {
		protocol tcp
		dport 1234
		timeout 5s
		size 1
		l3proto ip
	}

	ct expectation ctexpect6 {
		protocol tcp
		dport 1234
		timeout 5s
		size 1
		l3proto ip6
	}

	chain c {
		type filter hook forward priority filter; policy drop;
		ct state established accept
		meta l4proto tcp ct state related counter accept
		meta iifname r_c ct state new jump {
			tcp dport 2222 counter ct expectation set meta nfproto map { ipv4 : ctexpect4, ipv6 : ctexpect6 } counter accept
			counter accept
		}
		meta l4proto tcp counter reject with tcp reset
		meta l4proto { icmp, icmpv6 } accept
	}
}
EOF

ip netns exec "$C" ping -q -c 1 "$ip4_sr"
ip netns exec "$C" ping -q -c 1 "$ip6_sr"

set +e
echo check port is closed from outside.
connect_fail "$S" "$ip4_cr" 1234
connect_fail "$S" "[$ip6_cr]" 1234

test_expect "$ip4_cr" "$ip4_sr"	""
test_expect "[$ip6_cr]" "[$ip6_sr]" ""

ip netns exec $R conntrack -F
test_timeout

set -e
ip netns exec $R $NFT -f - <<-EOF
flush ruleset
table inet t {
	ct expectation ctexpect4 {
		protocol tcp
		dport 1234
		timeout 5s
		size 1
		l3proto ip
	}

	ct expectation ctexpect6 {
		protocol tcp
		dport 1234
		timeout 5s
		size 1
		l3proto ip6
	}
	chain c {
		type filter hook forward priority filter; policy drop;
		ct state established accept
		meta l4proto tcp ct state related counter accept
		meta iifname r_c ct state new accept
		meta l4proto tcp counter reject with tcp reset
		meta l4proto { icmp, icmpv6 } accept
	}

	chain masq {
		type nat hook postrouting priority srcnat; policy accept;
		meta oifname "r_s" counter masquerade
	}

	chain e {
		type filter hook postrouting priority 50000 ; policy accept;
		meta iifname r_c ct state new jump {
			tcp dport 2222 counter ct expectation set meta nfproto map { ipv4 : ctexpect4, ipv6 : ctexpect6 } counter accept
			counter accept
		}
	}
}
EOF

ip netns exec $R conntrack -F
echo "Test: conntrack expectation with masquerade"
set +e

# Skip here, this is a kernel bug / missing feature we can't
# test for.
rc=77
test_expect "$ip4_rs" "$ip4_sr" " with masquerade"
test_expect "[$ip6_rs]" "[$ip6_sr]" " with masquerade"

ip netns exec "$R" conntrack -L
rc=0
