/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  For more information, see <https://www.knot-dns.cz/>
 */

#include <assert.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "libknot/xdp/tcp.h"
#include "libknot/xdp/tcp_iobuf.h"
#include "libknot/attribute.h"
#include "libknot/error.h"
#include "libdnssec/random.h"
#include "contrib/macros.h"
#include "contrib/openbsd/siphash.h"
#include "contrib/ucw/lists.h"

static uint32_t get_timestamp(void)
{
	struct timespec t;
	clock_gettime(CLOCK_MONOTONIC, &t);
	uint64_t res = (uint64_t)t.tv_sec * 1000000;
	res += (uint64_t)t.tv_nsec / 1000;
	return res & 0xffffffff; // overflow does not matter since we are working with differences
}

static size_t sockaddr_data_len(const struct sockaddr_in6 *rem, const struct sockaddr_in6 *loc)
{
	assert(rem->sin6_family == loc->sin6_family);
	if (rem->sin6_family == AF_INET) {
		return offsetof(struct sockaddr_in, sin_zero);
	} else {
		assert(rem->sin6_family == AF_INET6);
		return offsetof(struct sockaddr_in6, sin6_scope_id);
	}
}

static uint64_t hash_four_tuple(const struct sockaddr_in6 *rem, const struct sockaddr_in6 *loc,
                                knot_tcp_table_t *table)
{
	size_t socka_data_len = sockaddr_data_len(rem, loc);
	SIPHASH_CTX ctx;
	SipHash24_Init(&ctx, (const SIPHASH_KEY *)(table->hash_secret));
	SipHash24_Update(&ctx, rem, socka_data_len);
	SipHash24_Update(&ctx, loc, socka_data_len);
	return SipHash24_End(&ctx);
}

static list_t *tcp_table_timeout(knot_tcp_table_t *table)
{
	return (list_t *)&table->conns[table->size];
}

static node_t *tcp_conn_node(knot_tcp_conn_t *conn)
{
	return (node_t *)&conn->list_node_placeholder;
}

static bool conn_removed(knot_tcp_conn_t *conn)
{
	return tcp_conn_node(conn)->next == NULL;
}

static void next_node_ptr(knot_tcp_conn_t **ptr)
{
	if (*ptr != NULL) {
		assert(!conn_removed(*ptr));

		*ptr = (*ptr)->list_node_placeholder.list_node_next;
		if ((*ptr)->list_node_placeholder.list_node_next == NULL) { // detected tail of list
			*ptr = NULL;
		}
	}
}

static void next_ptr_ibuf(knot_tcp_conn_t **ptr)
{
	do {
		next_node_ptr(ptr);
	} while (*ptr != NULL && (*ptr)->inbuf.iov_len == 0);
}

static void next_ptr_obuf(knot_tcp_conn_t **ptr)
{
	do {
		next_node_ptr(ptr);
	} while (*ptr != NULL && knot_tcp_outbufs_usage((*ptr)->outbufs) == 0);
}

_public_
knot_tcp_table_t *knot_tcp_table_new(size_t size, knot_tcp_table_t *secret_share)
{
	knot_tcp_table_t *table = calloc(1, sizeof(*table) + sizeof(list_t) +
	                                    size * sizeof(table->conns[0]));
	if (table == NULL) {
		return table;
	}

	table->size = size;
	init_list(tcp_table_timeout(table));

	assert(sizeof(table->hash_secret) == sizeof(SIPHASH_KEY));
	if (secret_share == NULL) {
		table->hash_secret[0] = dnssec_random_uint64_t();
		table->hash_secret[1] = dnssec_random_uint64_t();
	} else {
		table->hash_secret[0] = secret_share->hash_secret[0];
		table->hash_secret[1] = secret_share->hash_secret[1];
	}

	return table;
}

static void del_conn(knot_tcp_conn_t *conn)
{
	if (conn != NULL) {
		free(conn->inbuf.iov_base);
		while (conn->outbufs != NULL) {
			struct knot_tcp_outbuf *next = conn->outbufs->next;
			free(conn->outbufs);
			conn->outbufs = next;
		}
		free(conn);
	}
}

_public_
void knot_tcp_table_free(knot_tcp_table_t *table)
{
	if (table != NULL) {
		knot_tcp_conn_t *conn, *next;
		WALK_LIST_DELSAFE(conn, next, *tcp_table_timeout(table)) {
			del_conn(conn);
		}
		free(table);
	}
}

static knot_tcp_conn_t **tcp_table_lookup(const struct sockaddr_in6 *rem,
                                          const struct sockaddr_in6 *loc,
                                          uint64_t *hash, knot_tcp_table_t *table)
{
	if (*hash == 0) {
		*hash = hash_four_tuple(rem, loc, table);
	}
	size_t sdl = sockaddr_data_len(rem, loc);
	knot_tcp_conn_t **res = table->conns + (*hash % table->size);
	while (*res != NULL) {
		if (memcmp(&(*res)->ip_rem, rem, sdl) == 0 &&
		    memcmp(&(*res)->ip_loc, loc, sdl) == 0) {
			break;
		}
		res = &(*res)->next;
	}
	return res;
}

static knot_tcp_conn_t **tcp_table_re_lookup(knot_tcp_conn_t *conn,
                                             knot_tcp_table_t *table)
{
	uint64_t unused_hash = 0;
	knot_tcp_conn_t **res = tcp_table_lookup(&conn->ip_rem, &conn->ip_loc,
	                                         &unused_hash, table);
	assert(*res == conn);
	return res;
}

static void rem_align_pointers(knot_tcp_conn_t *to_rem, knot_tcp_table_t *table)
{
	assert(!conn_removed(to_rem));
	if (to_rem == table->next_close) {
		next_node_ptr(&table->next_close);
	}
	if (to_rem == table->next_ibuf) {
		next_ptr_ibuf(&table->next_ibuf);
	}
	if (to_rem == table->next_obuf) {
		next_ptr_obuf(&table->next_obuf);
	}
	if (to_rem == table->next_resend) {
		next_ptr_obuf(&table->next_resend);
	}
}

static void tcp_table_remove_conn(knot_tcp_conn_t **todel)
{
	rem_node(tcp_conn_node(*todel)); // remove from timeout double-linked list
	*todel = (*todel)->next; // remove from conn-table linked list
}

static void tcp_table_remove(knot_tcp_conn_t **todel, knot_tcp_table_t *table)
{
	assert(table->usage > 0);
	rem_align_pointers(*todel, table);
	table->inbufs_total -= buffer_alloc_size((*todel)->inbuf.iov_len);
	table->outbufs_total -= knot_tcp_outbufs_usage((*todel)->outbufs);
	tcp_table_remove_conn(todel);
	table->usage--;
}

static void conn_init_from_msg(knot_tcp_conn_t *conn, knot_xdp_msg_t *msg)
{
	memcpy(&conn->ip_rem, &msg->ip_from, sizeof(conn->ip_rem));
	memcpy(&conn->ip_loc, &msg->ip_to,   sizeof(conn->ip_loc));

	memcpy(&conn->last_eth_rem, &msg->eth_from, sizeof(conn->last_eth_rem));
	memcpy(&conn->last_eth_loc, &msg->eth_to,   sizeof(conn->last_eth_loc));

	conn->seqno = msg->seqno;
	conn->ackno = msg->ackno;
	conn->acked = msg->ackno;

	conn->last_active = get_timestamp();
	conn->state = XDP_TCP_NORMAL;
	conn->establish_rtt = 0;
	conn->flags = 0;

	memset(&conn->inbuf, 0, sizeof(conn->inbuf));
	memset(&conn->outbufs, 0, sizeof(conn->outbufs));
}

static void tcp_table_insert(knot_tcp_conn_t *conn, uint64_t hash,
                             knot_tcp_table_t *table)
{
	knot_tcp_conn_t **addto = table->conns + (hash % table->size);
	add_tail(tcp_table_timeout(table), tcp_conn_node(conn));
	if (table->next_close == NULL) {
		table->next_close = conn;
	}
	conn->next = *addto;
	*addto = conn;
	table->usage++;
}

// WARNING you shall ensure that it's not in the table already!
static int tcp_table_add(knot_xdp_msg_t *msg, uint64_t hash, knot_tcp_table_t *table,
                         knot_tcp_conn_t **res)
{
	knot_tcp_conn_t *c = malloc(sizeof(*c));
	if (c == NULL) {
		return KNOT_ENOMEM;
	}
	conn_init_from_msg(c, msg);
	tcp_table_insert(c, hash, table);
	*res = c;
	return KNOT_EOK;
}

static bool check_seq_ack(const knot_xdp_msg_t *msg, const knot_tcp_conn_t *conn)
{
	if (conn == NULL || conn->seqno != msg->seqno) {
		return false;
	}

	if (conn->acked <= conn->ackno) { // ackno does not wrap around uint32
		return (msg->ackno >= conn->acked && msg->ackno <= conn->ackno);
	} else { // this is more tricky
		return (msg->ackno >= conn->acked || msg->ackno <= conn->ackno);
	}
}

static void conn_update(knot_tcp_conn_t *conn, const knot_xdp_msg_t *msg)
{
	conn->seqno = knot_tcp_next_seqno(msg);
	memcpy(conn->last_eth_rem, msg->eth_from, sizeof(conn->last_eth_rem));
	memcpy(conn->last_eth_loc, msg->eth_to, sizeof(conn->last_eth_loc));
	conn->window_size = (uint32_t)msg->win * (1LU << conn->window_scale);

	uint32_t now = get_timestamp();
	if (conn->establish_rtt == 0 && conn->last_active != 0) {
		conn->establish_rtt = now - conn->last_active;
	}
	conn->last_active = now;
}

_public_
int knot_tcp_recv(knot_tcp_relay_t *relay, knot_xdp_msg_t *msg,
                  knot_tcp_table_t *tcp_table, knot_tcp_table_t *syn_table,
                  knot_tcp_ignore_t ignore)
{
	if (relay == NULL || msg == NULL || tcp_table == NULL) {
		return KNOT_EINVAL;
	}
	memset(relay, 0, sizeof(*relay));

	int ret = KNOT_EOK;

	if (!(msg->flags & KNOT_XDP_MSG_TCP)) {
		return KNOT_EOK;
	}

	uint64_t conn_hash = 0;
	knot_tcp_conn_t **pconn = tcp_table_lookup(&msg->ip_from, &msg->ip_to,
	                                           &conn_hash, tcp_table);
	knot_tcp_conn_t *conn = *pconn;
	bool seq_ack_match = check_seq_ack(msg, conn);
	if (seq_ack_match) {
		assert(conn->mss != 0);
		conn_update(conn, msg);

		rem_align_pointers(conn, tcp_table);
		rem_node(tcp_conn_node(conn));
		add_tail(tcp_table_timeout(tcp_table), tcp_conn_node(conn));

		if (msg->flags & KNOT_XDP_MSG_ACK) {
			conn->acked = msg->ackno;
			knot_tcp_outbufs_ack(&conn->outbufs, msg->ackno, &tcp_table->outbufs_total);
		}
	}

	relay->msg = msg;
	relay->conn = conn;

	// process incoming data
	if (seq_ack_match && (msg->flags & KNOT_XDP_MSG_ACK) && msg->payload.iov_len > 0) {
		if (!(ignore & XDP_TCP_IGNORE_DATA_ACK)) {
			relay->auto_answer = KNOT_XDP_MSG_ACK;
		}
		ret = knot_tcp_inbufs_upd(&conn->inbuf, msg->payload, false,
		                          &relay->inbf, &tcp_table->inbufs_total);
		if (ret != KNOT_EOK) {
			return ret;
		}
		if (conn->inbuf.iov_len > 0 && tcp_table->next_ibuf == NULL) {
			tcp_table->next_ibuf = conn;
		}
	}

	// process TCP connection state
	switch (msg->flags & (KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_ACK |
	                      KNOT_XDP_MSG_FIN | KNOT_XDP_MSG_RST)) {
	case KNOT_XDP_MSG_SYN:
	case (KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_ACK):
		if (conn == NULL) {
			bool synack = (msg->flags & KNOT_XDP_MSG_ACK);

			knot_tcp_table_t *add_table = tcp_table;
			if (syn_table != NULL) {
				if (synack) {
					break; // creating conn based on SYN+ACK is only for kxdpgun, disallow in knotd
				}
				add_table = syn_table;
				if (*tcp_table_lookup(&msg->ip_from, &msg->ip_to, &conn_hash, syn_table) != NULL) {
					break;
				}
			}

			ret = tcp_table_add(msg, conn_hash, add_table, &relay->conn);
			if (ret == KNOT_EOK) {
				relay->action = synack ? XDP_TCP_ESTABLISH : XDP_TCP_SYN;
				if (!(ignore & XDP_TCP_IGNORE_ESTABLISH)) {
					relay->auto_answer = synack ? KNOT_XDP_MSG_ACK : (KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_ACK);
				}

				conn = relay->conn;
				conn->state = synack ? XDP_TCP_NORMAL: XDP_TCP_ESTABLISHING;
				conn->mss = MAX(msg->mss, 536); // minimal MSS, most importantly not zero!
				conn->window_scale = msg->win_scale;
				conn_update(conn, msg);
				if (!synack) {
					conn->acked = dnssec_random_uint32_t();
					conn->ackno = conn->acked;
				}
			}
		} else {
			relay->auto_answer = KNOT_XDP_MSG_ACK;
		}
		break;
	case KNOT_XDP_MSG_ACK:
		if (!seq_ack_match) {
			if (syn_table != NULL && msg->payload.iov_len == 0 && conn == NULL &&
			    (pconn = tcp_table_lookup(&msg->ip_from, &msg->ip_to, &conn_hash, syn_table)) != NULL &&
			    (conn = *pconn) != NULL && check_seq_ack(msg, conn)) {
				// move conn from syn_table to tcp_table
				tcp_table_remove(pconn, syn_table);
				tcp_table_insert(conn, conn_hash, tcp_table);
				relay->conn = conn;
				relay->action = XDP_TCP_ESTABLISH;
				conn->state = XDP_TCP_NORMAL;
				conn_update(conn, msg);
			}
		} else {
			switch (conn->state) {
			case XDP_TCP_NORMAL:
			case XDP_TCP_CLOSING1: // just a mess, ignore
				break;
			case XDP_TCP_ESTABLISHING:
				conn->state = XDP_TCP_NORMAL;
				relay->action = XDP_TCP_ESTABLISH;
				break;
			case XDP_TCP_CLOSING2:
				if (msg->payload.iov_len == 0) { // otherwise ignore close
					tcp_table_remove(pconn, tcp_table);
					relay->answer = XDP_TCP_FREE;
				}
				break;
			}
		}
		break;
	case (KNOT_XDP_MSG_FIN | KNOT_XDP_MSG_ACK):
		if (ignore & XDP_TCP_IGNORE_FIN) {
			break;
		}
		if (!seq_ack_match) {
			if (conn != NULL) {
				relay->auto_answer = KNOT_XDP_MSG_RST;
				relay->auto_seqno = msg->ackno;
			} // else ignore. It would be better and possible, but no big value for the price of CPU.
		} else {
			if (conn->state == XDP_TCP_CLOSING1) {
				relay->action = XDP_TCP_CLOSE;
				relay->auto_answer = KNOT_XDP_MSG_ACK;
				relay->answer = XDP_TCP_FREE;
				tcp_table_remove(pconn, tcp_table);
			} else if (msg->payload.iov_len == 0) { // otherwise ignore FIN
				relay->action = XDP_TCP_CLOSE;
				relay->auto_answer = KNOT_XDP_MSG_FIN | KNOT_XDP_MSG_ACK;
				conn->state = XDP_TCP_CLOSING2;
			}
		}
		break;
	case KNOT_XDP_MSG_RST:
		if (conn != NULL && msg->seqno == conn->seqno) {
			relay->action = XDP_TCP_RESET;
			tcp_table_remove(pconn, tcp_table);
			relay->answer = XDP_TCP_FREE;
		} else if (conn != NULL) {
			relay->auto_answer = KNOT_XDP_MSG_ACK;
		}
		break;
	default:
		break;
	}

	return ret;
}

_public_
int knot_tcp_reply_data(knot_tcp_relay_t *relay, knot_tcp_table_t *tcp_table,
                        bool ignore_lastbyte, uint8_t *data, uint32_t len)
{
	if (relay == NULL || tcp_table == NULL || relay->conn == NULL || conn_removed(relay->conn)) {
		return KNOT_EINVAL;
	}
	int ret = knot_tcp_outbufs_add(&relay->conn->outbufs, data, len, ignore_lastbyte,
	                               relay->conn->mss, &tcp_table->outbufs_total);

	if (tcp_table->next_obuf == NULL && knot_tcp_outbufs_usage(relay->conn->outbufs) > 0) {
		tcp_table->next_obuf = relay->conn;
	}
	if (tcp_table->next_resend == NULL && knot_tcp_outbufs_usage(relay->conn->outbufs) > 0) {
		tcp_table->next_resend = relay->conn;
	}
	return ret;
}

static knot_xdp_msg_t *first_msg(knot_xdp_msg_t *msgs, uint32_t n_msgs)
{
	memset(msgs, 0, n_msgs * sizeof(*msgs));
	return msgs - 1; // will be incremented just before first use
}

static int send_msgs(knot_xdp_msg_t *msgs, uint32_t n_msgs, knot_xdp_socket_t *socket)
{
	assert(socket);
	assert(msgs);

	if (n_msgs > 0) {
		uint32_t unused;
		return knot_xdp_send(socket, msgs, n_msgs, &unused);
	}

	return KNOT_EOK;
}

static void msg_init_from_conn(knot_xdp_msg_t *msg, knot_tcp_conn_t *conn)
{
	memcpy( msg->eth_from, conn->last_eth_loc, sizeof(msg->eth_from));
	memcpy( msg->eth_to,   conn->last_eth_rem, sizeof(msg->eth_to));
	memcpy(&msg->ip_from, &conn->ip_loc,  sizeof(msg->ip_from));
	memcpy(&msg->ip_to,   &conn->ip_rem,  sizeof(msg->ip_to));

	msg->ackno = conn->seqno;
	msg->seqno = conn->ackno;

	msg->payload.iov_len = 0;

	msg->win_scale = 14; // maximum possible
	msg->win = 0xffff;
}

static int next_msg(knot_xdp_msg_t *msgs, uint32_t n_msgs, knot_xdp_msg_t **cur,
                    knot_xdp_socket_t *socket, knot_tcp_relay_t *rl)
{
	(*cur)++;
	if (*cur - msgs >= n_msgs) {
		int ret = send_msgs(msgs, n_msgs, socket);
		if (ret != KNOT_EOK) {
			return ret;
		}
		*cur = first_msg(msgs, n_msgs);
		(*cur)++;
	}

	knot_xdp_msg_t *msg = *cur;

	knot_xdp_msg_flag_t fl = KNOT_XDP_MSG_TCP;
	if (rl->conn->ip_loc.sin6_family == AF_INET6) {
		fl |= KNOT_XDP_MSG_IPV6;
	}
	if (rl->conn->state == XDP_TCP_ESTABLISHING) {
		fl |= KNOT_XDP_MSG_MSS | KNOT_XDP_MSG_WSC;
	}

	int ret = knot_xdp_send_alloc(socket, fl, msg);
	if (ret != KNOT_EOK) {
		return ret;
	}

	msg_init_from_conn(msg, rl->conn);

	return ret;
}

_public_
int knot_tcp_send(knot_xdp_socket_t *socket, knot_tcp_relay_t relays[],
                  uint32_t relay_count, uint32_t max_at_once)
{
	if (relay_count == 0) {
		return KNOT_EOK;
	}
	if (socket == NULL || relays == NULL) {
		return KNOT_EINVAL;
	}

	knot_xdp_msg_t msgs[max_at_once], *first = first_msg(msgs, max_at_once), *msg = first;

	for (uint32_t i = 0; i < relay_count; i++) {
		knot_tcp_relay_t *rl = &relays[i];

#define NEXT_MSG { \
	int ret = next_msg(msgs, max_at_once, &msg, socket, rl); \
	if (ret != KNOT_EOK) { return ret; } \
}

		if (rl->auto_answer != 0) {
			NEXT_MSG
			msg->flags |= rl->auto_answer;
			if (msg->flags & (KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_FIN)) {
				rl->conn->ackno++;
			}
			if (rl->auto_answer == KNOT_XDP_MSG_RST) {
				msg->seqno = rl->auto_seqno;
			}
		}

		switch (rl->answer & 0x0f) {
		case XDP_TCP_ESTABLISH:
			NEXT_MSG
			msg->flags |= KNOT_XDP_MSG_SYN;
			rl->conn->ackno++;
			break;
		case XDP_TCP_CLOSE:
			NEXT_MSG
			msg->flags |= (KNOT_XDP_MSG_FIN | KNOT_XDP_MSG_ACK);
			rl->conn->ackno++;
			rl->conn->state = XDP_TCP_CLOSING1;
			break;
		case XDP_TCP_RESET:
			NEXT_MSG
			msg->flags |= (KNOT_XDP_MSG_RST | KNOT_XDP_MSG_ACK);
			break;
		case XDP_TCP_NOOP:
		default:
			break;
		}

		size_t can_data = 0;
		knot_tcp_outbuf_t *ob;
		if (rl->conn != NULL) {
			knot_tcp_outbufs_can_send(rl->conn->outbufs, rl->conn->window_size,
			                          rl->answer == XDP_TCP_RESEND, &ob, &can_data);
		}
		while (can_data > 0) {
			NEXT_MSG
			msg->flags |= KNOT_XDP_MSG_ACK;
			msg->payload.iov_len = ob->len;
			memcpy(msg->payload.iov_base, ob->bytes, ob->len);

			if (!ob->sent) {
				assert(rl->conn->ackno == msg->seqno);
				rl->conn->ackno += msg->payload.iov_len;
			} else {
				msg->seqno = ob->seqno;
			}

			ob->sent = true;
			ob->seqno = msg->seqno;

			can_data--;
			ob = ob->next;
		}
#undef NEXT_MSG
	}

	return send_msgs(msgs, msg - first, socket);
}

static void sweep_reset(knot_tcp_table_t *tcp_table, knot_tcp_relay_t *rl,
                        ssize_t *free_conns, ssize_t *free_inbuf, ssize_t *free_outbuf,
                        knot_sweep_stats_t *stats, knot_sweep_counter_t counter)
{
	rl->answer = XDP_TCP_RESET | XDP_TCP_FREE;
	tcp_table_remove(tcp_table_re_lookup(rl->conn, tcp_table), tcp_table); // also updates tcp_table->next_*

	*free_conns -= 1;
	*free_inbuf -= buffer_alloc_size(rl->conn->inbuf.iov_len);
	*free_outbuf -= knot_tcp_outbufs_usage(rl->conn->outbufs);

	knot_sweep_stats_incr(stats, counter);
}

_public_
int knot_tcp_sweep(knot_tcp_table_t *tcp_table,
                   uint32_t close_timeout, uint32_t reset_timeout,
                   uint32_t resend_timeout, uint32_t limit_conn_count,
                   size_t limit_ibuf_size, size_t limit_obuf_size,
                   knot_tcp_relay_t *relays, uint32_t max_relays,
                   struct knot_sweep_stats *stats)
{
	if (tcp_table == NULL || relays == NULL || max_relays < 1) {
		return KNOT_EINVAL;
	}

	uint32_t now = get_timestamp();
	memset(relays, 0, max_relays * sizeof(*relays));
	knot_tcp_relay_t *rl = relays, *rl_max = rl + max_relays;

	ssize_t free_conns =  (ssize_t)(tcp_table->usage - limit_conn_count);
	ssize_t free_inbuf =  (ssize_t)(tcp_table->inbufs_total - MIN(limit_ibuf_size, SSIZE_MAX));
	ssize_t free_outbuf = (ssize_t)(tcp_table->outbufs_total - MIN(limit_obuf_size, SSIZE_MAX));

	// reset connections to free ibufs
	while (free_inbuf > 0 && rl != rl_max) {
		assert(tcp_table->next_ibuf != NULL);
		if (tcp_table->next_ibuf->inbuf.iov_len == 0) { // this conn might have get rid of ibuf in the meantime
			next_ptr_ibuf(&tcp_table->next_ibuf);
		}
		assert(tcp_table->next_ibuf != NULL);
		rl->conn = tcp_table->next_ibuf;
		sweep_reset(tcp_table, rl, &free_conns, &free_inbuf, &free_outbuf,
		            stats, KNOT_SWEEP_CTR_LIMIT_IBUF);
		rl++;
	}

	// reset connections to free obufs
	while (free_outbuf > 0 && rl != rl_max) {
		assert(tcp_table->next_obuf != NULL);
		if (knot_tcp_outbufs_usage(tcp_table->next_obuf->outbufs) == 0) {
			next_ptr_obuf(&tcp_table->next_obuf);
		}
		assert(tcp_table->next_obuf != NULL);
		rl->conn = tcp_table->next_obuf;
		sweep_reset(tcp_table, rl, &free_conns, &free_inbuf, &free_outbuf,
		            stats, KNOT_SWEEP_CTR_LIMIT_OBUF);
		rl++;
	}

	// reset connections to free their count, and old ones
	knot_tcp_conn_t *conn, *next;
	WALK_LIST_DELSAFE(conn, next, *tcp_table_timeout(tcp_table)) {
		bool active = now - conn->last_active < reset_timeout;
		if ((free_conns <= 0 && active) || rl == rl_max) {
			break;
		}

		knot_sweep_counter_t ctr = active ? KNOT_SWEEP_CTR_LIMIT_CONN :
		                                    KNOT_SWEEP_CTR_TIMEOUT_RST;
		rl->conn = conn;
		sweep_reset(tcp_table, rl, &free_conns, &free_inbuf, &free_outbuf,
		            stats, ctr);
		rl++;
	}

	// close old connections
	while (tcp_table->next_close != NULL &&
	       now - tcp_table->next_close->last_active >= close_timeout &&
	       rl != rl_max) {
		if (tcp_table->next_close->state != XDP_TCP_CLOSING1) {
			rl->conn = tcp_table->next_close;
			rl->answer = XDP_TCP_CLOSE;
			knot_sweep_stats_incr(stats, KNOT_SWEEP_CTR_TIMEOUT);
			rl++;
		}
		next_node_ptr(&tcp_table->next_close);
	}

	// resend unACKed data
	while (tcp_table->next_resend != NULL &&
	       now - tcp_table->next_resend->last_active >= resend_timeout &&
	       rl != rl_max) {
		rl->conn = tcp_table->next_resend;
		rl->answer = XDP_TCP_RESEND;
		rl++;
		next_ptr_obuf(&tcp_table->next_resend);
	}

	return KNOT_EOK;
}

_public_
void knot_tcp_cleanup(knot_tcp_table_t *tcp_table, knot_tcp_relay_t relays[],
                      uint32_t relay_count)
{
	(void)tcp_table;
	for (uint32_t i = 0; i < relay_count; i++) {
		if (relays[i].answer & XDP_TCP_FREE) {
			assert(conn_removed(relays[i].conn));
			assert(relays[i].conn != tcp_table->next_close);
			assert(relays[i].conn != tcp_table->next_ibuf);
			assert(relays[i].conn != tcp_table->next_obuf);
			assert(relays[i].conn != tcp_table->next_resend);

			del_conn(relays[i].conn);
		}
		free(relays[i].inbf);
	}
	memset(relays, 0, relay_count * sizeof(relays[0]));
}
