// SPDX-License-Identifier: GPL-2.0
/* Author: Dmitry Safonov <dima@arista.com> */
#include <inttypes.h>
#include "aolib.h"

static union tcp_addr local_addr;

static void __setup_lo_intf(const char *lo_intf,
			    const char *addr_str, uint8_t prefix)
{
	if (inet_pton(TEST_FAMILY, addr_str, &local_addr) != 1)
		test_error("Can't convert local ip address");

	if (ip_addr_add(lo_intf, TEST_FAMILY, local_addr, prefix))
		test_error("Failed to add %s ip address", lo_intf);

	if (link_set_up(lo_intf))
		test_error("Failed to bring %s up", lo_intf);
}

static void setup_lo_intf(const char *lo_intf)
{
#ifdef IPV6_TEST
	__setup_lo_intf(lo_intf, "::1", 128);
#else
	__setup_lo_intf(lo_intf, "127.0.0.1", 8);
#endif
}

static void tcp_self_connect(const char *tst, unsigned int port,
			     bool different_keyids, bool check_restore)
{
	struct tcp_ao_counters before_ao, after_ao;
	uint64_t before_aogood, after_aogood;
	struct netstat *ns_before, *ns_after;
	const size_t nr_packets = 20;
	struct tcp_ao_repair ao_img;
	struct tcp_sock_state img;
	sockaddr_af addr;
	int sk;

	tcp_addr_to_sockaddr_in(&addr, &local_addr, htons(port));

	sk = socket(test_family, SOCK_STREAM, IPPROTO_TCP);
	if (sk < 0)
		test_error("socket()");

	if (different_keyids) {
		if (test_add_key(sk, DEFAULT_TEST_PASSWORD, local_addr, -1, 5, 7))
			test_error("setsockopt(TCP_AO_ADD_KEY)");
		if (test_add_key(sk, DEFAULT_TEST_PASSWORD, local_addr, -1, 7, 5))
			test_error("setsockopt(TCP_AO_ADD_KEY)");
	} else {
		if (test_add_key(sk, DEFAULT_TEST_PASSWORD, local_addr, -1, 100, 100))
			test_error("setsockopt(TCP_AO_ADD_KEY)");
	}

	if (bind(sk, (struct sockaddr *)&addr, sizeof(addr)) < 0)
		test_error("bind()");

	ns_before = netstat_read();
	before_aogood = netstat_get(ns_before, "TCPAOGood", NULL);
	if (test_get_tcp_ao_counters(sk, &before_ao))
		test_error("test_get_tcp_ao_counters()");

	if (__test_connect_socket(sk, "lo", (struct sockaddr *)&addr,
				  sizeof(addr), TEST_TIMEOUT_SEC) < 0) {
		ns_after = netstat_read();
		netstat_print_diff(ns_before, ns_after);
		test_error("failed to connect()");
	}

	if (test_client_verify(sk, 100, nr_packets, TEST_TIMEOUT_SEC)) {
		test_fail("%s: tcp connection verify failed", tst);
		close(sk);
		return;
	}

	ns_after = netstat_read();
	after_aogood = netstat_get(ns_after, "TCPAOGood", NULL);
	if (test_get_tcp_ao_counters(sk, &after_ao))
		test_error("test_get_tcp_ao_counters()");
	if (!check_restore) {
		/* to debug: netstat_print_diff(ns_before, ns_after); */
		netstat_free(ns_before);
	}
	netstat_free(ns_after);

	if (after_aogood <= before_aogood) {
		test_fail("%s: TCPAOGood counter mismatch: %" PRIu64 " <= %" PRIu64,
			  tst, after_aogood, before_aogood);
		close(sk);
		return;
	}

	if (test_tcp_ao_counters_cmp(tst, &before_ao, &after_ao, TEST_CNT_GOOD)) {
		close(sk);
		return;
	}

	if (!check_restore) {
		test_ok("%s: connect TCPAOGood %" PRIu64 " => %" PRIu64,
				tst, before_aogood, after_aogood);
		close(sk);
		return;
	}

	test_enable_repair(sk);
	test_sock_checkpoint(sk, &img, &addr);
#ifdef IPV6_TEST
	addr.sin6_port = htons(port + 1);
#else
	addr.sin_port = htons(port + 1);
#endif
	test_ao_checkpoint(sk, &ao_img);
	test_kill_sk(sk);

	sk = socket(test_family, SOCK_STREAM, IPPROTO_TCP);
	if (sk < 0)
		test_error("socket()");

	test_enable_repair(sk);
	__test_sock_restore(sk, "lo", &img, &addr, &addr, sizeof(addr));
	if (different_keyids) {
		if (test_add_repaired_key(sk, DEFAULT_TEST_PASSWORD, 0,
					  local_addr, -1, 7, 5))
			test_error("setsockopt(TCP_AO_ADD_KEY)");
		if (test_add_repaired_key(sk, DEFAULT_TEST_PASSWORD, 0,
					  local_addr, -1, 5, 7))
			test_error("setsockopt(TCP_AO_ADD_KEY)");
	} else {
		if (test_add_repaired_key(sk, DEFAULT_TEST_PASSWORD, 0,
					  local_addr, -1, 100, 100))
			test_error("setsockopt(TCP_AO_ADD_KEY)");
	}
	test_ao_restore(sk, &ao_img);
	test_disable_repair(sk);
	test_sock_state_free(&img);
	if (test_client_verify(sk, 100, nr_packets, TEST_TIMEOUT_SEC)) {
		test_fail("%s: tcp connection verify failed", tst);
		close(sk);
		return;
	}
	ns_after = netstat_read();
	after_aogood = netstat_get(ns_after, "TCPAOGood", NULL);
	/* to debug: netstat_print_diff(ns_before, ns_after); */
	netstat_free(ns_before);
	netstat_free(ns_after);
	close(sk);
	if (after_aogood <= before_aogood) {
		test_fail("%s: TCPAOGood counter mismatch: %" PRIu64 " <= %" PRIu64,
			  tst, after_aogood, before_aogood);
		return;
	}
	test_ok("%s: connect TCPAOGood %" PRIu64 " => %" PRIu64,
			tst, before_aogood, after_aogood);
}

static void *client_fn(void *arg)
{
	unsigned int port = test_server_port;

	setup_lo_intf("lo");

	tcp_self_connect("self-connect(same keyids)", port++, false, false);

	/* expecting rnext to change based on the first segment RNext != Current */
	trace_ao_event_expect(TCP_AO_RNEXT_REQUEST, local_addr, local_addr,
			      port, port, 0, -1, -1, -1, -1, -1, 7, 5, -1);
	tcp_self_connect("self-connect(different keyids)", port++, true, false);
	tcp_self_connect("self-connect(restore)", port, false, true);
	port += 2; /* restore test restores over different port */
	trace_ao_event_expect(TCP_AO_RNEXT_REQUEST, local_addr, local_addr,
			      port, port, 0, -1, -1, -1, -1, -1, 7, 5, -1);
	/* intentionally on restore they are added to the socket in different order */
	trace_ao_event_expect(TCP_AO_RNEXT_REQUEST, local_addr, local_addr,
			      port + 1, port + 1, 0, -1, -1, -1, -1, -1, 5, 7, -1);
	tcp_self_connect("self-connect(restore, different keyids)", port, true, true);
	port += 2; /* restore test restores over different port */

	return NULL;
}

int main(int argc, char *argv[])
{
	test_init(5, client_fn, NULL);
	return 0;
}
