From 453308a6ca88d15b46c5fca2b6ee272d0f98eab0 Mon Sep 17 00:00:00 2001
From: Libor Peltan <libor.peltan@nic.cz>
Date: Thu, 20 May 2021 17:28:40 +0200
Subject: [PATCH] tests: XDP-TCP test of connection handling, states and stress

---
 Knot.files                   |   1 +
 src/libknot/xdp/bpf-user.h   |   5 +-
 src/libknot/xdp/tcp.c        |   7 +
 src/libknot/xdp/tcp.h        |   5 +
 src/libknot/xdp/xdp.c        |  44 ++++-
 src/libknot/xdp/xdp.h        |  20 +-
 tests/.gitignore             |   1 +
 tests/Makefile.am            |   5 +
 tests/libknot/test_xdp_tcp.c | 365 +++++++++++++++++++++++++++++++++++
 9 files changed, 447 insertions(+), 6 deletions(-)
 create mode 100644 tests/libknot/test_xdp_tcp.c

diff --git a/Knot.files b/Knot.files
index 8cfcf4aa8d..0b88e2c969 100644
--- a/Knot.files
+++ b/Knot.files
@@ -611,6 +611,7 @@ tests/libknot/test_rrset-wire.c
 tests/libknot/test_rrset.c
 tests/libknot/test_tsig.c
 tests/libknot/test_wire.c
+tests/libknot/test_xdp_tcp.c
 tests/libknot/test_yparser.c
 tests/libknot/test_ypschema.c
 tests/libknot/test_yptrafo.c
diff --git a/src/libknot/xdp/bpf-user.h b/src/libknot/xdp/bpf-user.h
index 743e33568b..43964b75f9 100644
--- a/src/libknot/xdp/bpf-user.h
+++ b/src/libknot/xdp/bpf-user.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -75,6 +75,9 @@ struct knot_xdp_socket {
 	/*! Interface context. */
 	const struct kxsk_iface *iface;
 
+	/*! If non-NULL, it's a mocked socket with this send function. */
+	void *send_mock;
+
 	/*! The kernel has to be woken up by a syscall indication. */
 	bool kernel_needs_wakeup;
 
diff --git a/src/libknot/xdp/tcp.c b/src/libknot/xdp/tcp.c
index 457553e86d..8604e749b6 100644
--- a/src/libknot/xdp/tcp.c
+++ b/src/libknot/xdp/tcp.c
@@ -112,6 +112,13 @@ static knot_tcp_conn_t **tcp_table_lookup(const struct sockaddr_in6 *rem, const
 	return res;
 }
 
+_public_
+knot_tcp_conn_t *knot_tcp_table_find(knot_tcp_table_t *table, knot_xdp_msg_t *msg_recv)
+{
+	uint64_t unused;
+	return *tcp_table_lookup(&msg_recv->ip_from, &msg_recv->ip_to, &unused, table);
+}
+
 static void tcp_table_del(knot_tcp_conn_t **todel)
 {
 	knot_tcp_conn_t *conn = *todel;
diff --git a/src/libknot/xdp/tcp.h b/src/libknot/xdp/tcp.h
index 7db63b1174..9f2c73b9ce 100644
--- a/src/libknot/xdp/tcp.h
+++ b/src/libknot/xdp/tcp.h
@@ -118,6 +118,11 @@ knot_tcp_table_t *knot_tcp_table_new(size_t size);
  */
 void knot_tcp_table_free(knot_tcp_table_t *t);
 
+/*!
+ * \brief Find connection related to incomming message.
+ */
+knot_tcp_conn_t *knot_tcp_table_find(knot_tcp_table_t *table, knot_xdp_msg_t *msg_recv);
+
 /*!
  * \brief Process received packets, send ACKs, pick incoming data.
  *
diff --git a/src/libknot/xdp/xdp.c b/src/libknot/xdp/xdp.c
index 24d420c9f6..9921e99023 100644
--- a/src/libknot/xdp/xdp.c
+++ b/src/libknot/xdp/xdp.c
@@ -199,12 +199,33 @@ int knot_xdp_init(knot_xdp_socket_t **socket, const char *if_name, int if_queue,
 	return ret;
 }
 
+_public_
+int knot_xdp_init_mock(knot_xdp_socket_t **socket, knot_xdp_send_mock_f send_mock)
+{
+	if (socket == NULL || send_mock == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	*socket = calloc(1, sizeof(**socket));
+	if (*socket == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	(*socket)->send_mock = send_mock;
+
+	return KNOT_EOK;
+}
+
 _public_
 void knot_xdp_deinit(knot_xdp_socket_t *socket)
 {
 	if (socket == NULL) {
 		return;
 	}
+	if (unlikely(socket->send_mock != NULL)) {
+		free(socket);
+		return;
+	}
 
 	kxsk_socket_stop(socket->iface);
 	xsk_socket__delete(socket->xsk);
@@ -235,7 +256,7 @@ static void tx_free_relative(struct kxsk_umem *umem, uint64_t addr_relative)
 _public_
 void knot_xdp_send_prepare(knot_xdp_socket_t *socket)
 {
-	if (socket == NULL) {
+	if (socket == NULL || unlikely(socket->send_mock != NULL)) {
 		return;
 	}
 
@@ -257,8 +278,11 @@ void knot_xdp_send_prepare(knot_xdp_socket_t *socket)
 	xsk_ring_cons__release(cq, completed);
 }
 
-static struct umem_frame *alloc_tx_frame(struct kxsk_umem *umem)
+static struct umem_frame *alloc_tx_frame(struct kxsk_umem *umem, void *send_mock)
 {
+	if (unlikely(send_mock != NULL)) {
+		return malloc(sizeof(struct umem_frame));
+	}
 	if (unlikely(umem->tx_free_count == 0)) {
 		return NULL;
 	}
@@ -282,7 +306,7 @@ int knot_xdp_send_alloc(knot_xdp_socket_t *socket, knot_xdp_msg_flag_t flags,
 		return KNOT_EINVAL;
 	}
 
-	struct umem_frame *uframe = alloc_tx_frame(socket->umem);
+	struct umem_frame *uframe = alloc_tx_frame(socket->umem, socket->send_mock);
 	if (uframe == NULL) {
 		return KNOT_ENOMEM;
 	}
@@ -301,7 +325,7 @@ int knot_xdp_reply_alloc(knot_xdp_socket_t *socket, const knot_xdp_msg_t *query,
 		return KNOT_EINVAL;
 	}
 
-	struct umem_frame *uframe = alloc_tx_frame(socket->umem);
+	struct umem_frame *uframe = alloc_tx_frame(socket->umem, socket->send_mock);
 	if (uframe == NULL) {
 		return KNOT_ENOMEM;
 	}
@@ -314,6 +338,10 @@ int knot_xdp_reply_alloc(knot_xdp_socket_t *socket, const knot_xdp_msg_t *query,
 
 static void free_unsent(knot_xdp_socket_t *socket, const knot_xdp_msg_t *msg)
 {
+	if (unlikely(socket->send_mock != NULL)) {
+		free(msg->payload.iov_base - prot_write_hdrs_len(msg));
+		return;
+	}
 	uint64_t addr_relative = (uint8_t *)msg->payload.iov_base
 	                         - socket->umem->frames->bytes;
 	tx_free_relative(socket->umem, addr_relative);
@@ -326,6 +354,14 @@ int knot_xdp_send(knot_xdp_socket_t *socket, const knot_xdp_msg_t msgs[],
 	if (socket == NULL || msgs == NULL || sent == NULL) {
 		return KNOT_EINVAL;
 	}
+	if (unlikely(socket->send_mock != NULL)) {
+		knot_xdp_send_mock_f send_mock = socket->send_mock;
+		int ret = send_mock(socket, msgs, count, sent);
+		for (uint32_t i = 0; i < count; ++i) {
+			free_unsent(socket, &msgs[i]);
+		}
+		return ret;
+	}
 
 	/* Now we want to do something close to
 	 *   xsk_ring_prod__reserve(&socket->tx, count, *idx)
diff --git a/src/libknot/xdp/xdp.h b/src/libknot/xdp/xdp.h
index aa9ad644ef..7bff7c1c76 100644
--- a/src/libknot/xdp/xdp.h
+++ b/src/libknot/xdp/xdp.h
@@ -55,10 +55,18 @@ typedef enum {
 /*! \brief Context structure for one XDP socket. */
 typedef struct knot_xdp_socket knot_xdp_socket_t;
 
+/*!
+ * \brief A mocked XDP send function.
+ *
+ * \note This must correspond to the prototype of knot_xdp_send().
+ */
+typedef int (*knot_xdp_send_mock_f)(knot_xdp_socket_t *, const knot_xdp_msg_t[],
+                                    uint32_t, uint32_t *);
+
 /*!
  * \brief Initialize XDP socket.
  *
- * \param socket       Socket ctx.
+ * \param socket       XDP socket.
  * \param if_name      Name of the net iface (e.g. eth0).
  * \param if_queue     Network card queue to be used (normally 1 socket per each queue).
  * \param listen_port  Port to listen on, or KNOT_XDP_LISTEN_PORT_* flag.
@@ -69,6 +77,16 @@ typedef struct knot_xdp_socket knot_xdp_socket_t;
 int knot_xdp_init(knot_xdp_socket_t **socket, const char *if_name, int if_queue,
                   uint32_t listen_port, knot_xdp_load_bpf_t load_bpf);
 
+/*!
+ * \brief Initialize mocked XDP socket.
+ *
+ * \param socket       XDP socket.
+ * \param send_mock    Mocked send function.
+ *
+ * \return KNOT_E*
+ */
+int knot_xdp_init_mock(knot_xdp_socket_t **socket, knot_xdp_send_mock_f send_mock);
+
 /*!
  * \brief De-init XDP socket.
  *
diff --git a/tests/.gitignore b/tests/.gitignore
index a3cff1a236..ef9af5cb69 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -81,6 +81,7 @@
 /libknot/test_rrset
 /libknot/test_rrset-wire
 /libknot/test_tsig
+/libknot/test_xdp_tcp
 /libknot/test_yparser
 /libknot/test_ypschema
 /libknot/test_yptrafo
diff --git a/tests/Makefile.am b/tests/Makefile.am
index eeeab755ae..60f025e02c 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -155,6 +155,11 @@ check_PROGRAMS += \
 	libknot/test_yptrafo			\
 	libknot/test_wire
 
+if ENABLE_XDP
+check_PROGRAMS += \
+	libknot/test_xdp_tcp
+endif ENABLE_XDP
+
 if HAVE_LIBUTILS
 check_PROGRAMS += \
 	utils/test_cert				\
diff --git a/tests/libknot/test_xdp_tcp.c b/tests/libknot/test_xdp_tcp.c
new file mode 100644
index 0000000000..aeed4ee31b
--- /dev/null
+++ b/tests/libknot/test_xdp_tcp.c
@@ -0,0 +1,365 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <unistd.h>
+
+#include "tap/basic.h"
+#include "contrib/macros.h"
+#include "libknot/error.h"
+#include "libknot/xdp/msg_init.h"
+
+#include "libknot/xdp/tcp.h"
+
+knot_tcp_table_t *test_table = NULL;
+#define TEST_TABLE_SIZE 100
+
+size_t sent_acks = 0;
+size_t sent_rsts = 0;
+size_t sent_syns = 0;
+size_t sent_fins = 0;
+uint32_t sent_seqno = 0;
+uint32_t sent_ackno = 0;
+
+knot_xdp_socket_t *test_sock = NULL;
+
+struct sockaddr_in test_addr = { AF_INET, 0, { 127 + (1 << 24) }, { 0 } };
+
+knot_tcp_conn_t *test_conn = NULL;
+
+static int mock_send(knot_xdp_socket_t *sock, const knot_xdp_msg_t msgs[],
+                     uint32_t n_msgs, uint32_t *sent)
+{
+	UNUSED(sock);
+	UNUSED(sent);
+	ok(n_msgs <= 20, "send: not too many at once");
+	for (uint32_t i = 0; i < n_msgs; i++) {
+		const knot_xdp_msg_t *msg = msgs + i;
+
+		ok(msg->flags & KNOT_XDP_MSG_TCP, "send: is TCP message");
+		ok(msg->payload.iov_len == 0, "send: is empty payload");
+
+		if (msg->flags & KNOT_XDP_MSG_RST) {
+			ok(!(msg->flags & KNOT_XDP_MSG_ACK), "send: no RST+ACK");
+			sent_rsts++;
+		} else if (msg->flags & KNOT_XDP_MSG_SYN) {
+			ok(msg->flags & KNOT_XDP_MSG_ACK, "send: is SYN+ACK");
+			sent_syns++;
+		} else if (msg->flags & KNOT_XDP_MSG_FIN) {
+			ok(msg->flags & KNOT_XDP_MSG_ACK, "send: FIN has always ACK");
+			sent_fins++;
+		} else {
+			ok(msg->flags & KNOT_XDP_MSG_ACK, "send: is ACK");
+			sent_acks++;
+		}
+
+		sent_seqno = msg->seqno;
+		sent_ackno = msg->ackno;
+	}
+	return KNOT_EOK;
+}
+
+static int mock_send_nocheck(knot_xdp_socket_t *sock, const knot_xdp_msg_t msgs[],
+                             uint32_t n_msgs, uint32_t *sent)
+{
+	UNUSED(sock);
+	UNUSED(sent);
+	for (uint32_t i = 0; i < n_msgs; i++) {
+		const knot_xdp_msg_t *msg = msgs + i;
+		if (msg->flags & KNOT_XDP_MSG_RST) {
+			sent_rsts++;
+		} else if (msg->flags & KNOT_XDP_MSG_SYN) {
+			sent_syns++;
+		} else if (msg->flags & KNOT_XDP_MSG_FIN) {
+			sent_fins++;
+		} else {
+			sent_acks++;
+		}
+		sent_seqno = msg->seqno;
+		sent_ackno = msg->ackno;
+	}
+	return KNOT_EOK;
+}
+
+static void clean_table(void)
+{
+	(void)knot_xdp_tcp_cleanup(test_table, 0, UINT32_MAX, NULL);
+}
+
+static void clean_sent(void)
+{
+	sent_acks = 0;
+	sent_rsts = 0;
+	sent_syns = 0;
+	sent_fins = 0;
+}
+
+static void check_sent(size_t expect_acks, size_t expect_rsts, size_t expect_syns, size_t expect_fins)
+{
+	is_int(expect_acks, sent_acks, "sent ACKs");
+	is_int(expect_rsts, sent_rsts, "sent RSTs");
+	is_int(expect_syns, sent_syns, "sent SYNs");
+	is_int(expect_fins, sent_fins, "sent FINs");
+	clean_sent();
+}
+
+static void prepare_msg(knot_xdp_msg_t *msg, int flags, uint16_t sport, uint16_t dport)
+{
+	msg_init(msg, flags | KNOT_XDP_MSG_TCP);
+	memcpy(&msg->ip_from, &test_addr, sizeof(test_addr));
+	memcpy(&msg->ip_to, &test_addr, sizeof(test_addr));
+	msg->ip_from.sin6_port = htobe16(sport);
+	msg->ip_to.sin6_port = htobe16(dport);
+}
+
+static void prepare_seqack(knot_xdp_msg_t *msg, int seq_shift, int ack_shift)
+{
+	msg->seqno = sent_ackno + seq_shift;
+	msg->ackno = sent_seqno + ack_shift;
+}
+
+static void prepare_data(knot_xdp_msg_t *msg, const char *bytes, size_t n)
+{
+	msg->payload.iov_len = n;
+	msg->payload.iov_base = (void *)bytes;
+}
+
+void test_syn(void)
+{
+	knot_xdp_msg_t msg;
+	tcp_relay_dynarray_t relays = { 0 };
+	prepare_msg(&msg, KNOT_XDP_MSG_SYN, 1, 2);
+	int ret = knot_xdp_tcp_relay(test_sock, &msg, 1, test_table, NULL, &relays, NULL);
+	is_int(KNOT_EOK, ret, "SYN: relay OK");
+	is_int(msg.seqno + 1, sent_ackno, "SYN: ackno");
+	check_sent(0, 0, 1, 0);
+	is_int(1, relays.size, "SYN: one relay");
+	knot_tcp_relay_t *rl = &tcp_relay_dynarray_arr(&relays)[0];
+	is_int(XDP_TCP_SYN, rl->action, "SYN: relay action");
+	is_int(XDP_TCP_NOOP, rl->answer, "SYN: relay answer");
+	is_int(0, rl->data.iov_len, "SYN: no payload");
+	is_int(1, test_table->usage, "SYN: one connection in table");
+	knot_tcp_conn_t *conn = knot_tcp_table_find(test_table, &msg);
+	ok(conn != NULL, "SYN: connection present");
+	ok(conn == rl->conn, "SYN: relay points to connection");
+	is_int(XDP_TCP_ESTABLISHING, conn->state, "SYN: connection state");
+	ok(memcmp(&conn->ip_rem, &msg.ip_from, sizeof(msg.ip_from)) == 0, "SYN: conn IP from");
+	ok(memcmp(&conn->ip_loc, &msg.ip_to, sizeof(msg.ip_to)) == 0, "SYN: conn IP to");
+
+	knot_xdp_tcp_relay_free(&relays);
+	test_conn = conn;
+}
+
+void test_establish(void)
+{
+	knot_xdp_msg_t msg;
+	tcp_relay_dynarray_t relays = { 0 };
+	prepare_msg(&msg, KNOT_XDP_MSG_ACK, 1, 2);
+	prepare_seqack(&msg, 0, 1);
+	int ret = knot_xdp_tcp_relay(test_sock, &msg, 1, test_table, NULL, &relays, NULL);
+	is_int(KNOT_EOK, ret, "establish: relay OK");
+	check_sent(0, 0, 0, 0);
+	is_int(0, relays.size, "establish: no relay");
+	/*knot_tcp_relay_t *rl = &tcp_relay_dynarray_arr(&relays)[0];
+	is_int(XDP_TCP_ESTABLISH, rl->action, "establish: relay action");
+	ok(rl->conn != NULL, "establish: connection present");
+	ok(rl->conn == test_conn, "establish: same connection");
+	is_int(XDP_TCP_NORMAL, rl->conn->state, "establish: connection state");*/
+
+	knot_xdp_tcp_relay_free(&relays);
+	clean_table();
+}
+
+void test_syn_ack(void)
+{
+	knot_xdp_msg_t msg;
+	tcp_relay_dynarray_t relays = { 0 };
+	prepare_msg(&msg, KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_ACK, 1000, 2000);
+	int ret = knot_xdp_tcp_relay(test_sock, &msg, 1, test_table, NULL, &relays, NULL);
+	is_int(KNOT_EOK, ret, "SYN+ACK: relay OK");
+	is_int(msg.seqno + 1, sent_ackno, "SYN+ACK: ackno");
+	check_sent(1, 0, 0, 0);
+	is_int(1, relays.size, "SYN+ACK: one relay");
+	knot_tcp_relay_t *rl = &tcp_relay_dynarray_arr(&relays)[0];
+	is_int(XDP_TCP_ESTABLISH, rl->action, "SYN+ACK: relay action");
+	ok(rl->conn != NULL, "SYN+ACK: connection present");
+
+	test_conn = rl->conn;
+	knot_xdp_tcp_relay_free(&relays);
+}
+
+void test_data_fragments(void)
+{
+	knot_xdp_msg_t msgs[4];
+	tcp_relay_dynarray_t relays = { 0 };
+
+	// first msg contains one whole payload and one fragment
+	prepare_msg(&msgs[0], KNOT_XDP_MSG_ACK, 1000, 2000);
+	prepare_seqack(&msgs[0], 0, 0);
+	prepare_data(&msgs[0], "\x00\x03""xyz""\x00\x04""ab", 9);
+
+	// second msg contains just fragment not completing anything
+	prepare_msg(&msgs[1], KNOT_XDP_MSG_ACK, 1000, 2000);
+	prepare_seqack(&msgs[1], 9, 0);
+	prepare_data(&msgs[1], "c", 1);
+
+	// third msg finishes fragment, contains one whole, and starts new fragment by just half of length info
+	prepare_msg(&msgs[2], KNOT_XDP_MSG_ACK, 1000, 2000);
+	prepare_seqack(&msgs[2], 10, 0);
+	prepare_data(&msgs[2], "d""\x00\x01""i""\x00", 5);
+
+	// fourth msg completes fragment and starts never-finishing one
+	prepare_msg(&msgs[3], KNOT_XDP_MSG_ACK, 1000, 2000);
+	prepare_seqack(&msgs[3], 15, 0);
+	prepare_data(&msgs[3], "\x02""AB""\xff\xff""abcdefghijklmnopqrstuvwxyz...", 34);
+
+	int ret = knot_xdp_tcp_relay(test_sock, msgs, sizeof(msgs) / sizeof(msgs[0]), test_table, NULL, &relays, NULL);
+	is_int(KNOT_EOK, ret, "fragments: relay OK");
+	is_int(msgs[3].ackno, sent_seqno, "fragments: seqno");
+	is_int(msgs[3].seqno + msgs[3].payload.iov_len, sent_ackno, "fragments: ackno");
+	check_sent(4, 0, 0, 0);
+	knot_tcp_relay_t *rls = tcp_relay_dynarray_arr(&relays);
+
+	is_int(XDP_TCP_DATA, rls[0].action, "fragments0: action");
+	is_int(XDP_TCP_NOOP, rls[0].answer, "fragments0: answer");
+	is_int(3, rls[0].data.iov_len, "fragments0: data length");
+	ok(memcmp("xyz", rls[0].data.iov_base, rls[0].data.iov_len) == 0, "fragments0: data");
+	ok(rls[0].conn != NULL, "fragments0: connection present");
+	ok(rls[0].conn == test_conn, "fragments0: same connection");
+
+	is_int(XDP_TCP_DATA, rls[1].action, "fragments1: action");
+	is_int(4, rls[1].data.iov_len, "fragments1: data length");
+	ok(memcmp("abcd", rls[1].data.iov_base, rls[1].data.iov_len) == 0, "fragments1: data");
+	ok(rls[1].conn == test_conn, "fragments1: same connection");
+
+	is_int(XDP_TCP_DATA, rls[2].action, "fragments2: action");
+	is_int(1, rls[2].data.iov_len, "fragments2: data length");
+	ok(memcmp("i", rls[2].data.iov_base, rls[2].data.iov_len) == 0, "fragments2: data");
+	ok(rls[2].conn == test_conn, "fragments2: same connection");
+
+	is_int(XDP_TCP_DATA, rls[3].action, "fragments3: action");
+	is_int(2, rls[3].data.iov_len, "fragments3: data length");
+	ok(memcmp("AB", rls[3].data.iov_base, rls[3].data.iov_len) == 0, "fragments3: data");
+	ok(rls[3].conn == test_conn, "fragments3: same connection");
+
+	knot_xdp_tcp_relay_free(&relays);
+}
+
+void test_close(void)
+{
+	size_t conns_pre = test_table->usage;
+
+	knot_xdp_msg_t msg;
+	tcp_relay_dynarray_t relays = { 0 };
+	prepare_msg(&msg, KNOT_XDP_MSG_FIN | KNOT_XDP_MSG_ACK, be16toh(test_conn->ip_rem.sin6_port), be16toh(test_conn->ip_loc.sin6_port));
+	prepare_seqack(&msg, 0, 0);
+	int ret = knot_xdp_tcp_relay(test_sock, &msg, 1, test_table, NULL, &relays, NULL);
+	is_int(KNOT_EOK, ret, "close: relay 1 OK");
+	check_sent(0, 0, 0, 1);
+	is_int(1, relays.size, "close: one relay");
+	knot_tcp_relay_t *rl = &tcp_relay_dynarray_arr(&relays)[0];
+	is_int(XDP_TCP_CLOSE, rl->action, "close: relay action");
+	ok(rl->conn == test_conn, "close: same connection");
+	is_int(XDP_TCP_CLOSING, rl->conn->state, "close: conn state");
+	knot_xdp_tcp_relay_free(&relays);
+
+	msg.flags &= ~KNOT_XDP_MSG_FIN;
+	prepare_seqack(&msg, 0, 0);
+	ret = knot_xdp_tcp_relay(test_sock, &msg, 1, test_table, NULL, &relays, NULL);
+	is_int(KNOT_EOK, ret, "close: relay 2 OK");
+	check_sent(0, 0, 0, 0);
+	is_int(conns_pre - 1, test_table->usage, "close: connection removed");
+	is_int(conns_pre - 1, list_size(&test_table->timeout), "close: timeout list size");
+}
+
+void test_many(void)
+{
+	size_t CONNS = test_table->size * test_table->size;
+	size_t i_survive = CONNS / 2;
+	uint32_t timeout_time = 1000000;
+
+	knot_xdp_msg_t *msgs = malloc(CONNS * sizeof(*msgs));
+	assert(msgs != NULL);
+	for (size_t i = 0; i < CONNS; i++) {
+		prepare_msg(&msgs[i], KNOT_XDP_MSG_SYN, i + 2, 1);
+	}
+
+	tcp_relay_dynarray_t relays = { 0 };
+	int ret = knot_xdp_tcp_relay(test_sock, msgs, CONNS, test_table, NULL, &relays, NULL);
+	is_int(KNOT_EOK, ret, "many: relay OK");
+	check_sent(0, 0, CONNS, 0);
+	is_int(CONNS, relays.size, "many: relays count");
+	is_int(CONNS, test_table->usage, "many: table usage");
+
+	knot_xdp_tcp_relay_free(&relays);
+	usleep(timeout_time);
+	knot_xdp_msg_t *survive = &msgs[i_survive];
+	survive->flags = (KNOT_XDP_MSG_TCP | KNOT_XDP_MSG_ACK);
+	knot_tcp_conn_t *surv_conn = knot_tcp_table_find(test_table, survive);
+	survive->seqno = surv_conn->seqno;
+	survive->ackno = surv_conn->ackno;
+	prepare_data(survive, "\x00\x00", 2);
+	(void)knot_xdp_tcp_relay(test_sock, survive, 1, test_table, NULL, &relays, NULL);
+	is_int(1, relays.size, "many/survivor: one relay");
+	knot_tcp_relay_t *rl = &tcp_relay_dynarray_arr(&relays)[0];
+	clean_sent();
+
+	uint32_t reset_count = 0;
+	ret = knot_xdp_tcp_timeout(test_table, test_sock, UINT32_MAX, timeout_time, UINT32_MAX, 0, 0, &reset_count);
+	is_int(KNOT_EOK, ret, "many/timeout1: OK");
+	is_int(0, reset_count, "may/timeout1: reset count");
+	check_sent(0, 0, 0, CONNS - 1);
+
+	ret = knot_xdp_tcp_timeout(test_table, test_sock, UINT32_MAX, UINT32_MAX, timeout_time, 0, 0, &reset_count);
+	is_int(KNOT_EOK, ret, "many/timeout2: OK");
+	is_int(CONNS - 1, reset_count, "may/timeout2: reset count");
+	check_sent(0, CONNS - 1, 0, 0);
+	is_int(1, test_table->usage, "many/timeout: one survivor");
+	is_int(1, list_size(&test_table->timeout), "many/timeout: one survivor in timeout list");
+	ok(surv_conn != NULL, "many/timeout: survivor connection present");
+	ok(surv_conn == rl->conn, "many/timeout: same connection");
+
+	free(msgs);
+}
+
+int main(int argc, char *argv[])
+{
+	UNUSED(argc);
+	UNUSED(argv);
+	plan_lazy();
+
+	test_table = knot_tcp_table_new(TEST_TABLE_SIZE);
+	assert(test_table != NULL);
+
+	int ret = knot_xdp_init_mock(&test_sock, mock_send);
+	assert(ret == KNOT_EOK);
+
+	test_syn();
+	test_establish();
+
+	test_syn_ack();
+	test_data_fragments();
+	test_close();
+
+	knot_xdp_deinit(test_sock);
+	ret = knot_xdp_init_mock(&test_sock, mock_send_nocheck);
+	assert(ret == KNOT_EOK);
+	test_many();
+
+	knot_xdp_deinit(test_sock);
+	knot_tcp_table_free(test_table);
+
+	return 0;
+}
-- 
GitLab